Temporarily turn on Cascade Delete

clip art of 
 a double-quote character

Question

This has probably been asked before, but I couldn’t find the answer.

I have a complex datamodel in sql-server-20XX (more likely to be something old) in which cascade delete has been turned off (by design).

A large chunk of our database has been sold (I can make the distinction on the top-level) and there are reasons why we want to delete that data. I want to do that in a single script, in which I first turn on cascade delete, than delete all the data, than turn off cascade delete again, but I can’t find the correct syntax.

Relevant things to mention:

  • I want to delete about 40% of the rows from 10-15 tables in total, that are all somehow 1:n-1:n…-coupled to the Organisations-table
  • Performance is not relevant, since the application is not used outside of business hours

The tables must stay, I need to get rid of 10/33 rows in the Organisations-table and I need to get rid of ALL the rows in Orders, users… etc that are coupled to those organisations

asked 2022-05-20 by Ward


Answer

SQL Server doesn’t let you ALTER CONSTRAINT to alter an existing constraint to cascade on delete. However, SQL Server will happily let you create identical Foreign Key constraints. So rather than having to mess with your existing FK constraints, you can simply create some new ones, which will help you do your cleanup, then you can drop those special-purpose constraints when you’re done.

Example data

Consider this very simple schema:

CREATE DATABASE [CascadeOnDelete];
GO

USE [CascadeOnDelete];

CREATE TABLE dbo.Customers (
    CustomerID int,
    CustomerName nvarchar(100),
    CONSTRAINT PK_Customers PRIMARY KEY CLUSTERED (CustomerID)
    );
INSERT dbo.Customers (CustomerID, CustomerName)
VALUES (1,'Grandpa Joe'),(2,'Grandma Josephine'),
        (3,'Grandpa George'),(4,'Grandma Gerorgina'),
        (5,'Charlie Bucket');

CREATE TABLE dbo.Orders(
    OrderID int,
    CustomerID int
        CONSTRAINT FK_Orders_CustomerID 
        REFERENCES Customers(CustomerID),
    OrderDetails nvarchar(100),
    CONSTRAINT PK_Parents PRIMARY KEY CLUSTERED (OrderID)
    );
INSERT dbo.Orders (OrderID, CustomerID, OrderDetails)
VALUES (1,1,'Wonka bar'),
       (2,5,'Wonka bar'),
       (3,5,'Wonka Scrumdiddlyumptious Bar');

SELECT * FROM Customers;
SELECT * FROM Orders;

If you try to delete Grampa Joe, you’ll get an error because Grampa Joe has an order:

DELETE c
FROM dbo.Customers AS c
WHERE CustomerID = 1;

/*
Msg 547, Level 16, State 0, Line 33
The DELETE statement conflicted with the REFERENCE constraint "FK_Orders_CustomerID". The conflict occurred in database "CascadeOnDelete", table "dbo.Orders", column 'CustomerID'.
*/

Now that we have that set up, we can script out the rest of what we need:

The solution

First, we can use some dynamic SQL to generate the script for every FK in your database. I’m appending the names with _CASCADEDELETES to identify them. I’m also creating these WITH NOCHECK and ON DELETE CASCADE regardless of the original FK definition. You would take the output of this script, review it, and run it to duplicate every FK in your database with these additions:

--adapted from https://www.mssqltips.com/sqlservertip/3347/drop-and-recreate-all-foreign-key-constraints-in-sql-server/
SELECT N'ALTER TABLE ' 
       + QUOTENAME(cs.name) + '.' + QUOTENAME(ct.name) + N' WITH NOCHECK'
       + N'    ADD CONSTRAINT ' + QUOTENAME(fk.name + N'_CASCADEDELETES') 
       + N'    FOREIGN KEY (' 
            + STUFF((SELECT ',' + QUOTENAME(c.name)
                    FROM sys.columns AS c 
                    JOIN sys.foreign_key_columns AS fkc 
                        ON fkc.parent_column_id = c.column_id
                        AND fkc.parent_object_id = c.object_id
                    WHERE fkc.constraint_object_id = fk.object_id
                    ORDER BY fkc.constraint_column_id 
                    FOR XML PATH(N''), TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 1, N'')
        + N') REFERENCES ' + QUOTENAME(rs.name) + '.' + QUOTENAME(rt.name)
        + N'(' 
            + STUFF((SELECT ',' + QUOTENAME(c.name)
                     FROM sys.columns AS c 
                     JOIN sys.foreign_key_columns AS fkc 
                        ON fkc.referenced_column_id = c.column_id
                        AND fkc.referenced_object_id = c.[object_id]
                    WHERE fkc.constraint_object_id = fk.[object_id]
                    ORDER BY fkc.constraint_column_id 
                    FOR XML PATH(N''), TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 1, N'') 
        + N')'
        + N' ON DELETE CASCADE;'
FROM sys.foreign_keys AS fk
INNER JOIN sys.tables AS rt -- referenced table
  ON fk.referenced_object_id = rt.[object_id]
INNER JOIN sys.schemas AS rs 
  ON rt.[schema_id] = rs.[schema_id]
INNER JOIN sys.tables AS ct -- constraint table
  ON fk.parent_object_id = ct.[object_id]
INNER JOIN sys.schemas AS cs 
  ON ct.[schema_id] = cs.[schema_id]
WHERE rt.is_ms_shipped = 0 AND ct.is_ms_shipped = 0;

In our example database, we only have one FK, so the output produces a new FK for that one FK:

ALTER TABLE [dbo].[Orders] WITH NOCHECK
    ADD CONSTRAINT [FK_Orders_CustomerID_CASCADEDELETES] FOREIGN KEY ([CustomerID]) 
        REFERENCES [dbo].[Customers]([CustomerID]) 
        ON DELETE CASCADE;

After creating that FK, we can simply run our original delete again, to delete Grampa Joe from the Customers table, and it will cascade & delete the Orders as well:

DELETE c
FROM dbo.Customers AS c
WHERE CustomerID = 1;

SELECT *
FROM dbo.Orders

And we’re done! The last step is to simply drop the duplicate FKs we created. We can use another script to generate the DROP commands, just like we did with the creation. We can simply review & run the output from this script to drop FKs based on the appended _CASCADE DELETES we added to the name:

--adapted from https://www.mssqltips.com/sqlservertip/3347/drop-and-recreate-all-foreign-key-constraints-in-sql-server/
SELECT N'
ALTER TABLE ' + QUOTENAME(cs.name) + '.' + QUOTENAME(ct.name) 
    + ' DROP CONSTRAINT ' + QUOTENAME(fk.name) + ';'
FROM sys.foreign_keys AS fk
INNER JOIN sys.tables AS ct
  ON fk.parent_object_id = ct.[object_id]
INNER JOIN sys.schemas AS cs 
  ON ct.[schema_id] = cs.[schema_id]
WHERE fk.name LIKE '%CASCADEDELETES';

The benefit of this is that you don’t have to worry about getting the original FKs back to exactly how they were when you started, since you never have to touch them. This can be particularly beneficial if all your FKs are fully trusted, and you want to avoid potential blocking or runtime issues of recreating all the FKs WITH CHECK to ensure they are trusted at the end.

answered 2022-05-20 by Andy Mallon