Question

I have a database full of customer data. It's so big that it's really cumbersome to operate on, and I'd rather just slim it down to 10% of the customers, which is plenty for development. I have an awful lot of tables and I don't want to alter them all with "ON DELETE CASCADE", especially because this is a one-time deal.

Can I do a delete operation that cascades through all my tables without setting them up first? If not, what is my best option?

Was it helpful?

Solution

Combining your advice and a script I found online, I made a procedure that will produce SQL you can run to perform a cascaded delete regardless of ON DELETE CASCADE. It was probably a big waste of time, but I had a good time writing it. An advantage of doing it this way is, you can put a GO statement between each line, and it doesn't have to be one big transaction. The original was a recursive procedure; this one unrolls the recursion into a stack table.

create procedure usp_delete_cascade (
    @base_table_name varchar(200), @base_criteria nvarchar(1000)
)
as begin
    -- Adapted from http://www.sqlteam.com/article/performing-a-cascade-delete-in-sql-server-7
    -- Expects the name of a table, and a conditional for selecting rows
    -- within that table that you want deleted.
    -- Produces SQL that, when run, deletes all table rows referencing the ones
    -- you initially selected, cascading into any number of tables,
    -- without the need for "ON DELETE CASCADE".
    -- Does not appear to work with self-referencing tables, but it will
    -- delete everything beneath them.
    -- To make it easy on the server, put a "GO" statement between each line.

    declare @to_delete table (
        id int identity(1, 1) primary key not null,
        criteria nvarchar(1000) not null,
        table_name varchar(200) not null,
        processed bit not null,
        delete_sql varchar(1000)
    )

    insert into @to_delete (criteria, table_name, processed) values (@base_criteria, @base_table_name, 0)

    declare @id int, @criteria nvarchar(1000), @table_name varchar(200)
    while exists(select 1 from @to_delete where processed = 0) begin
        select top 1 @id = id, @criteria = criteria, @table_name = table_name from @to_delete where processed = 0 order by id desc

        insert into @to_delete (criteria, table_name, processed)
            select referencing_column.name + ' in (select [' + referenced_column.name + '] from [' + @table_name +'] where ' + @criteria + ')',
                referencing_table.name,
                0
            from  sys.foreign_key_columns fk
                inner join sys.columns referencing_column on fk.parent_object_id = referencing_column.object_id 
                    and fk.parent_column_id = referencing_column.column_id 
                inner join  sys.columns referenced_column on fk.referenced_object_id = referenced_column.object_id 
                    and fk.referenced_column_id = referenced_column.column_id 
                inner join  sys.objects referencing_table on fk.parent_object_id = referencing_table.object_id 
                inner join  sys.objects referenced_table on fk.referenced_object_id = referenced_table.object_id 
                inner join  sys.objects constraint_object on fk.constraint_object_id = constraint_object.object_id
            where referenced_table.name = @table_name
                and referencing_table.name != referenced_table.name

        update @to_delete set
            processed = 1
        where id = @id
    end

    select 'print ''deleting from ' + table_name + '...''; delete from [' + table_name + '] where ' + criteria from @to_delete order by id desc
end

exec usp_delete_cascade 'root_table_name', 'id = 123'

OTHER TIPS

Unless you want to maintain all related queries as proposed by Chris, the ON DELETE CASCADE is by far the quickest and the most direct solution. And if you don't want it to be permanent, why don't you have some T-SQL code that will switch this option on and off like here

  1. remove the original Tbl_A_MyFK constraint (without the ON DELETE CASCADE)

    ALTER TABLE Tbl_A DROP CONSTRAINT Tbl_A_MyFK

  2. set the constraint Tbl_A_MyFK with the ON DELETE CASCADE

    ALTER TABLE Tbl_A ADD CONSTRAINT Tbl_A_MyFK FOREIGN KEY (MyFK) REFERENCES Tbl_B(Column) ON DELETE CASCADE

  3. Here you can do your delete

    DELETE FROM Tbl_A WHERE ...

  4. drop your constraint Tbl_A_MyFK

    ALTER TABLE Tbl_A DROP CONSTRAINT Tbl_A_MyFK

  5. set the constraint Tbl_A_MyFK without the ON DELETE CASCADE

    ALTER TABLE Tbl_A ADD CONSTRAINT Tbl_A_MyFK FOREIGN KEY (MyFK) REFERENCES (Tbl_B)

Here's a version of the accepted answer optimised for sparsely populated data models. It checks for the existence of data in a FK chain before adding it to the deletion list. I use it to clean up test data.

Don't use it in an active transactional db- it will hold locks way too long.

/*
-- ============================================================================
-- Purpose: Performs a cascading hard-delete.
--          Not for use on an active transactional database- it holds locks for too long.
--          (http://stackoverflow.com/questions/116968/in-sql-server-2005-can-i-do-a-cascade-delete-without-setting-the-property-on-my)
-- eg:
exec dbo.hp_Common_Delete 'tblConsumer', 'Surname = ''TestDxOverdueOneReviewWm''', 1
-- ============================================================================
*/
create proc [dbo].[hp_Common_Delete]
(
    @TableName sysname, 
    @Where nvarchar(4000),  -- Shouldn't include 'where' keyword, e.g. Surname = 'smith', NOT where Surname = 'smith'
    @IsDebug bit = 0
)
as
set nocount on

begin try
    -- Prepare tables to store deletion criteria.  
    -- #tmp_to_delete stores criteria that is tested for results before being added to #to_delete
    create table #to_delete
    (
        id int identity(1, 1) primary key not null,
        criteria nvarchar(4000) not null,
        table_name sysname not null,
        processed bit not null default(0)
    )
    create table #tmp_to_delete 
    (
        id int primary key identity(1,1), 
        criteria nvarchar(4000) not null, 
        table_name sysname not null
    )

    -- Open a transaction (it'll be a long one- don't use this on production!)
    -- We need a transaction around criteria generation because we only 
    -- retain criteria that has rows in the db, and we don't want that to change under us.
    begin tran
        -- If the top-level table meets the deletion criteria, add it
        declare @Sql nvarchar(4000)
        set @Sql = 'if exists(select top(1) * from ' + @TableName + ' where ' + @Where + ') 
            insert #to_delete (criteria, table_name) values (''' + replace(@Where, '''', '''''') + ''', ''' + @TableName + ''')'
        exec (@Sql)

        -- Loop over deletion table, walking foreign keys to generate delete targets
        declare @id int, @tmp_id int, @criteria nvarchar(4000), @new_criteria nvarchar(4000), @table_name sysname, @new_table_name sysname
        while exists(select 1 from #to_delete where processed = 0) 
        begin
            -- Grab table/criteria to work on
            select  top(1) @id = id, 
                    @criteria = criteria, 
                    @table_name = table_name 
            from    #to_delete 
            where   processed = 0 
            order by id desc

            -- Insert all immediate child tables into a temp table for processing
            insert  #tmp_to_delete
            select  referencing_column.name + ' in (select [' + referenced_column.name + '] from [' + @table_name +'] where ' + @criteria + ')',
                    referencing_table.name
            from  sys.foreign_key_columns fk
                    inner join sys.columns referencing_column on fk.parent_object_id = referencing_column.object_id 
                            and fk.parent_column_id = referencing_column.column_id 
                    inner join  sys.columns referenced_column on fk.referenced_object_id = referenced_column.object_id 
                            and fk.referenced_column_id = referenced_column.column_id 
                    inner join  sys.objects referencing_table on fk.parent_object_id = referencing_table.object_id 
                    inner join  sys.objects referenced_table on fk.referenced_object_id = referenced_table.object_id 
                    inner join  sys.objects constraint_object on fk.constraint_object_id = constraint_object.object_id
            where referenced_table.name = @table_name
                    and referencing_table.name != referenced_table.name

            -- Loop on child table criteria, and insert them into delete table if they have records in the db
            select @tmp_id = max(id) from #tmp_to_delete
            while (@tmp_id >= 1)
            begin
                select @new_criteria = criteria, @new_table_name = table_name from #tmp_to_delete where id = @tmp_id
                set @Sql = 'if exists(select top(1) * from ' + @new_table_name + ' where ' + @new_criteria + ') 
                    insert #to_delete (criteria, table_name) values (''' + replace(@new_criteria, '''', '''''') + ''', ''' + @new_table_name + ''')'
                exec (@Sql)

                set @tmp_id = @tmp_id - 1
            end
            truncate table #tmp_to_delete

            -- Move to next record
            update  #to_delete 
            set     processed = 1
            where   id = @id
        end

        -- We have a list of all tables requiring deletion.  Actually delete now.
        select @id = max(id) from #to_delete 
        while (@id >= 1)
        begin
            select @criteria = criteria, @table_name = table_name from #to_delete where id = @id
            set @Sql = 'delete from [' + @table_name + '] where ' + @criteria
            if (@IsDebug = 1) print @Sql
            exec (@Sql)

            -- Next record
            set @id = @id - 1
        end
    commit
end try

begin catch
    -- Any error results in a rollback of the entire job
    if (@@trancount > 0) rollback

    declare @message nvarchar(2047), @errorProcedure nvarchar(126), @errorMessage nvarchar(2048), @errorNumber int, @errorSeverity int, @errorState int, @errorLine int
    select  @errorProcedure = isnull(error_procedure(), N'hp_Common_Delete'), 
            @errorMessage = isnull(error_message(), N'hp_Common_Delete unable to determine error message'), 
            @errorNumber = error_number(), @errorSeverity = error_severity(), @errorState = error_state(), @errorLine = error_line()

    -- Prepare error information as it would be output in SQL Mgt Studio
    declare @event nvarchar(2047)
    select  @event =    'Msg ' + isnull(cast(@errorNumber as varchar), 'null') + 
                        ', Level ' + isnull(cast(@errorSeverity as varchar), 'null') + 
                        ', State ' + isnull(cast(@errorState as varchar), 'null') + 
                        ', Procedure ' + isnull(@errorProcedure, 'null') + 
                        ', Line ' + isnull(cast(@errorLine as varchar), 'null') + 
                        ': ' + isnull(@errorMessage, '@ErrorMessage null')
    print   @event

    -- Re-raise error to ensure admin/job runners understand there was a failure
    raiserror(@errorMessage, @errorSeverity, @errorState)
end catch

Go into SQL Server Management Studio and right-click the database. Select Tasks->Generate Scripts. Click Next twice. On the Options window choose set it to generate CREATE statements only, and put everything to False except for the Foreign Keys. Click Next. Select Tables and Click Next again. Click the "Select All" button and click Next then Finish and send the script to your choice of a query window or file (don't use the clipboard, since it might be a big script). Now remove all of the script that adds the tables and you should be left with a script to create your foreign keys.

Make a copy of that script because it is how you'll restore your database to its current state. Use a search and replace to add the ON DELETE CASCADE to the end of each constraint. This might vary depending on how your FKs are currently set up and you might need to do some manual editing.

Repeat the script generation, but this time set it to generate DROP statements only. Be sure to manually remove the table drops that are generated. Run the drops, then run your edited creates to make them all cascade on delete. Do your deletes, run the drop script again and then run the script that you saved off at the start.

Also - MAKE A BACKUP OF YOUR DB FIRST! Even if it's just a dev database, it will save you some headache if part of the script isn't quite right.

Hope this helps!

BTW - you should definitely do some testing with your full test data as another poster suggested, but I can see why you might not need that for initial development. Just don't forget to include that as part of QA at some point.

I usually just hand write the queries to delete the records I don't want and save that as a .sql file for future reference. The pseudocode is:

  1. select id's of records from the main table that I want to delete into a temp table
  2. write a delete query for each related table which joins to the temp table.
  3. write a delete query for the main table joining to my temp table.

My suggestion is to go ahead and write a script that will add the on delete cascade to each relationship in the database while exporting a list of modified relationships. Then you can reverse the process and remove the on delete cascade command on each table in the list.

Personally if you are going to leave the records in production, I would also leave them in development. Otherwise you may write code that works fine when the recordset is small but times out when faced with the real recordset.

But if you are determined to do this, I would copy the id field of the records you want to dete from the main table first to a work table. Then I would take each related table and write a delete joining to that worktable to only delete those records. Finish up with the parent table. Make sure this ia written in a script and saved so the next time you want to do a similar thing to your test data, you can easily run it without having to figure out what are the reated tables that need records deleted from them.

Taking the accepted answer a bit further, I had the need to do this across tables in different schemas. I have updated the script to include schema in the outputted delete scripts.

CREATE PROCEDURE usp_delete_cascade (
        @base_table_schema varchar(100), @base_table_name varchar(200), @base_criteria nvarchar(1000)
)
as begin

        -- Expects the name of a table, and a conditional for selecting rows
        -- within that table that you want deleted.
        -- Produces SQL that, when run, deletes all table rows referencing the ones
        -- you initially selected, cascading into any number of tables,
        -- without the need for "ON DELETE CASCADE".
        -- Does not appear to work with self-referencing tables, but it will
        -- delete everything beneath them.
        -- To make it easy on the server, put a "GO" statement between each line.

        declare @to_delete table (
                id int identity(1, 1) primary key not null,
                criteria nvarchar(1000) not null,
                table_schema varchar(100),
                table_name varchar(200) not null,
                processed bit not null,
                delete_sql varchar(1000)
        )

        insert into @to_delete (criteria, table_schema, table_name, processed) values (@base_criteria, @base_table_schema, @base_table_name, 0)

        declare @id int, @criteria nvarchar(1000), @table_name varchar(200), @table_schema varchar(100)
        while exists(select 1 from @to_delete where processed = 0) begin
                select top 1 @id = id, @criteria = criteria, @table_name = table_name, @table_schema = table_schema from @to_delete where processed = 0 order by id desc

                insert into @to_delete (criteria, table_schema, table_name, processed)
                        select referencing_column.name + ' in (select [' + referenced_column.name + '] from [' + @table_schema + '].[' + @table_name +'] where ' + @criteria + ')',
                                schematable.name,
                                referencing_table.name,
                                0
                        from  sys.foreign_key_columns fk
                                inner join sys.columns referencing_column on fk.parent_object_id = referencing_column.object_id 
                                        and fk.parent_column_id = referencing_column.column_id 
                                inner join  sys.columns referenced_column on fk.referenced_object_id = referenced_column.object_id 
                                        and fk.referenced_column_id = referenced_column.column_id 
                                inner join  sys.objects referencing_table on fk.parent_object_id = referencing_table.object_id 
                                inner join sys.schemas schematable on referencing_table.schema_id = schematable.schema_id
                                inner join  sys.objects referenced_table on fk.referenced_object_id = referenced_table.object_id 
                                inner join  sys.objects constraint_object on fk.constraint_object_id = constraint_object.object_id
                        where referenced_table.name = @table_name
                                and referencing_table.name != referenced_table.name

                update @to_delete set
                        processed = 1
                where id = @id
        end

        select 'print ''deleting from ' + table_name + '...''; delete from [' + table_schema + '].[' + table_name + '] where ' + criteria from @to_delete order by id desc
end

exec usp_delete_cascade 'schema', 'RootTable', 'Id = 123'
exec usp_delete_cascade 'schema', 'RootTable', 'GuidId = ''A7202F84-FA57-4355-B499-1F8718E29058'''

Kevin post is incomplete, his t-sql sp only prints the command, to execute these command, before last end add this

DECLARE @commandText VARCHAR(8000)
        DECLARE curDeletes CURSOR FOR
            select 'delete from [' + table_name + '] where ' + criteria from @to_delete order by id desc

        OPEN curDeletes
        FETCH NEXT FROM curDeletes
        INTO
            @commandText

        WHILE(@@FETCH_STATUS=0)
        BEGIN
            EXEC (@commandText)
            FETCH NEXT FROM curDeletes INTO @commandText
        END
        CLOSE curDeletes
        DEALLOCATE curDeletes

Expansion of croisharp's answer to take triggers into consideration, i.e. schema-aware solution that disables all affecting triggers, deletes rows, and enables the triggers.

CREATE PROCEDURE usp_delete_cascade (
@base_table_schema varchar(100),
@base_table_name varchar(200),
@base_criteria nvarchar(1000)
)
as begin

    -- Expects the name of a table, and a conditional for selecting rows
    -- within that table that you want deleted.
    -- Produces SQL that, when run, deletes all table rows referencing the ones
    -- you initially selected, cascading into any number of tables,
    -- without the need for "ON DELETE CASCADE".
    -- Does not appear to work with self-referencing tables, but it will
    -- delete everything beneath them.
    -- To make it easy on the server, put a "GO" statement between each line.

    declare @to_delete table (
            id int identity(1, 1) primary key not null,
            criteria nvarchar(1000) not null,
            table_schema varchar(100),
            table_name varchar(200) not null,
            processed bit not null,
            delete_sql varchar(1000)
    )

    insert into @to_delete (criteria, table_schema, table_name, processed) values (@base_criteria, @base_table_schema, @base_table_name, 0)

    declare @id int, @criteria nvarchar(1000), @table_name varchar(200), @table_schema varchar(100)
    while exists(select 1 from @to_delete where processed = 0) begin
            select top 1 @id = id, @criteria = criteria, @table_name = table_name, @table_schema = table_schema from @to_delete where processed = 0 order by id desc

            insert into @to_delete (criteria, table_schema, table_name, processed)
                    select referencing_column.name + ' in (select [' + referenced_column.name + '] from [' + @table_schema + '].[' + @table_name +'] where ' + @criteria + ')',
                            schematable.name,
                            referencing_table.name,
                            0
                    from  sys.foreign_key_columns fk
                            inner join sys.columns referencing_column on fk.parent_object_id = referencing_column.object_id 
                                    and fk.parent_column_id = referencing_column.column_id 
                            inner join  sys.columns referenced_column on fk.referenced_object_id = referenced_column.object_id 
                                    and fk.referenced_column_id = referenced_column.column_id 
                            inner join  sys.objects referencing_table on fk.parent_object_id = referencing_table.object_id 
                            inner join sys.schemas schematable on referencing_table.schema_id = schematable.schema_id
                            inner join  sys.objects referenced_table on fk.referenced_object_id = referenced_table.object_id 
                            inner join  sys.objects constraint_object on fk.constraint_object_id = constraint_object.object_id
                    where referenced_table.name = @table_name
                            and referencing_table.name != referenced_table.name

            update @to_delete set
                    processed = 1
            where id = @id
    end

    select 'print ''deleting from ' + table_name + '...''; delete from [' + table_schema + '].[' + table_name + '] where ' + criteria from @to_delete order by id desc

    DECLARE @commandText VARCHAR(8000), @triggerOn VARCHAR(8000), @triggerOff VARCHAR(8000)
    DECLARE curDeletes CURSOR FOR
        select
            'DELETE FROM [' + table_schema + '].[' + table_name + '] WHERE ' + criteria,
            'ALTER TABLE [' + table_schema + '].[' + table_name + '] DISABLE TRIGGER ALL',
            'ALTER TABLE [' + table_schema + '].[' + table_name + '] ENABLE TRIGGER ALL'
        from @to_delete order by id desc

    OPEN curDeletes
    FETCH NEXT FROM curDeletes INTO @commandText, @triggerOff, @triggerOn

    WHILE(@@FETCH_STATUS=0)
    BEGIN
        EXEC (@triggerOff)
        EXEC (@commandText)
        EXEC (@triggerOn)
        FETCH NEXT FROM curDeletes INTO @commandText, @triggerOff, @triggerOn
    END
    CLOSE curDeletes
    DEALLOCATE curDeletes
end

after select you have to build and execute the actual delete

declare @deleteSql nvarchar(1200)
declare delete_cursor cursor for
select table_name, criteria 
from @to_delete
order by id desc

open delete_cursor

fetch next from delete_cursor
into @table_name, @criteria

while @@fetch_status = 0
begin
 select @deleteSql = 'delete from ' + @table_name + ' where ' + @criteria
 --print @deleteSql
-- exec sp_execute @deleteSql
EXEC SP_EXECUTESQL @deleteSql

 fetch next from delete_cursor
 into @table_name, @criteria
end
close delete_cursor
deallocate delete_cursor

Post here a script that will work with foreign keys contain more than one column.

create procedure usp_delete_cascade (
@TableName varchar(200), @Where nvarchar(1000)
) as begin

declare @to_delete table (
    id int identity(1, 1) primary key not null,
    criteria nvarchar(1000) not null,
    table_name varchar(200) not null,
    processed bit not null default(0),
    delete_sql varchar(1000)
)
            DECLARE @MyCursor CURSOR

declare         @referencing_column_name varchar(1000)
declare         @referencing_table_name varchar(1000)
 declare @Sql nvarchar(4000)
insert into @to_delete (criteria, table_name) values ('', @TableName)


declare @id int, @criteria nvarchar(1000), @table_name varchar(200)
while exists(select 1 from @to_delete where processed = 0) begin
    select top 1 @id = id, @criteria = criteria, @table_name = table_name from @to_delete where processed = 0 order by id desc
        SET @MyCursor = CURSOR FAST_FORWARD
        FOR
        select referencing_column.name as column_name,
            referencing_table.name as table_name
        from  sys.foreign_key_columns fk
            inner join sys.columns referencing_column on fk.parent_object_id = referencing_column.object_id 
                and fk.parent_column_id = referencing_column.column_id 
            inner join  sys.columns referenced_column on fk.referenced_object_id = referenced_column.object_id 
                and fk.referenced_column_id = referenced_column.column_id 
            inner join  sys.objects referencing_table on fk.parent_object_id = referencing_table.object_id 
            inner join  sys.objects referenced_table on fk.referenced_object_id = referenced_table.object_id 
            inner join  sys.objects constraint_object on fk.constraint_object_id = constraint_object.object_id
        where referenced_table.name = @table_name
            and referencing_table.name != referenced_table.name

        OPEN @MyCursor
        FETCH NEXT FROM @MYCursor
        INTO @referencing_column_name, @referencing_table_name

        WHILE @@FETCH_STATUS = 0

        BEGIN
            PRINT @referencing_column_name
            PRINT @referencing_table_name
                    update @to_delete set criteria = criteria + ' AND '+@table_name+'.'+@referencing_column_name+'='+ @referencing_table_name+'.'+@referencing_column_name
                    where table_name = @referencing_table_name

                    if(@@ROWCOUNT = 0)
                    BEGIN
                            --if(@id <> 1)
                            --BEGIN
                                insert into @to_delete (criteria, table_name)
                                VALUES( ' LEFT JOIN '+@table_name+' ON '+@table_name+'.'+@referencing_column_name+'='+ @referencing_table_name+'.'+@referencing_column_name+ @criteria,
                                @referencing_table_name
                                )
                            --END
                            --ELSE
                            --BEGIN
                                --insert into @to_delete (criteria, table_name)
                                --VALUES( ' LEFT JOIN '+@table_name+' ON '+@table_name+'.'+@referencing_column_name+'='+ @referencing_table_name+'.'+@referencing_column_name,
                                --@referencing_table_name
                                --)
                            --END
                    END
                        FETCH NEXT FROM @MYCursor
            INTO @referencing_column_name, @referencing_table_name
        END


        CLOSE @MyCursor 
        DEALLOCATE @MyCursor 
    update @to_delete set
        processed = 1
    where id = @id
end

--select 'print ''deleting from ' + table_name + '...''; delete from [' + table_name + '] where ' + criteria from @to_delete order by id desc

--select id, table_name, criteria, @Where from @to_delete order by id desc

select @id = max(id) from @to_delete
while (@id >= 1)
begin
    select @criteria = criteria, @table_name = table_name from @to_delete where id = @id
    set @Sql = 'delete [' + @table_name + '] from [' + @table_name + '] ' + @criteria+' WHERE '+@Where
    exec (@Sql)
    PRINT @Sql

    -- Next record
    set @id = @id - 1
end
end

This script has two issues: 1. You must indicate the condition 1=1 in order to delete all table base. 2. This creates the direct relations with the base table only. If the final table has another table parent relation, the the delete fail

DELETE FROM [dbo].[table2] WHERE TableID in (select [ID] from [dbo].[table3] where 1=1)

If table2 has a parent relation table1

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top