I need to get all the tables that were changed in a database, including the rows' IDs that were updated/added/removed.
So, if I have table table1 and table2, and rows with ID 15 and 16 were changed in table1, and rows with IDs 200 and 201 were changed in table2, I want to run a script that returns:
+-----------+-------+
| tableName | rowId |
+-----------+-------+
| table1 | 15 |
| table1 | 16 |
| table2 | 200 |
| table2 | 201 |
+-----------+-------+
I managed to find this script:
set nocount on;
-- We want to check for changes since the previous version
--declare #prevTrackingVersion int = INSERT_YOUR_PREV_VERSION_HERE
-- Comment out this line if you know the previous version
declare #prevTrackingVersion int = CHANGE_TRACKING_CURRENT_VERSION() - 1
-- Get a list of table with change tracking enabled
declare #trackedTables as table (name nvarchar(1000));
insert into #trackedTables (name)
select sys.tables.name from sys.change_tracking_tables
join sys.tables ON tables.object_id = change_tracking_tables.object_id
-- This will be the list of tables with changes
declare #changedTables as table (name nvarchar(1000));
-- For each table name in tracked tables
declare #tableName nvarchar(1000)
while exists(select top 1 * from #trackedTables)
begin
-- Set the current table name
set #tableName = (select top 1 name from #trackedTables order by name asc);
print #tableName
-- Determine if the table has changed since the previous version
declare #sql nvarchar(250)
declare #retVal int
set #sql = 'select #retVal = count(*) from changetable(changes ' + #tableName + ', ' + cast(#prevTrackingVersion as varchar) + ') as changedTable'
exec sp_executesql #sql, N'#retVal int output', #retVal output
print #retVal
if #retval > 0
begin
insert into #changedTables (name) select #tableName
end
-- Delete the current table name
delete from #trackedTables where name = #tableName;
end
select * from #changedTables;
However, it only outputs the names of the tables that were modified.
So, I know that this command:
SELECT * FROM CHANGETABLE(CHANGES table1, 0) as CT;
outputs this:
+----------------------+----+
| SYS_CHANGE_OPERATION | ID |
+----------------------+----+
| I | 15 |
| I | 16 |
+----------------------+----+
In the script, this same command is used:
set #sql = 'select #retVal = count(*) from changetable(changes ' + #tableName + ', ' + cast(#prevTrackingVersion as varchar) + ') as changedTable'
but it only gets the count, and then checks if it is greater than 0 (if there is any modifications for this table):
if #retval > 0
begin
insert into #changedTables (name) select #tableName
end
However, as mentioned, I need to not only get the tables modified, but the IDs of the rows that were modified.
I feel like I need to JOIN, but I'm not really sure how to work this out.
Thanks
What you need to do is -
Find out all the tracking tables and their primary keys.
The result returned from CHANGETABLE includes the primary key of the tracking table, NOT ROWID as you thought.
For example, if table t1's PK is defined on t1.id, then CHANGETABLE for t1 will return "id" column; if table t2's PK is defined on (id1, id2), then CHANGETABLE for t2 will return "id1" and "id2".
Try to run CHANGETABLE for one table can help you understand what I am saying -
select *
from CHANGETABLE(CHANGES your_table, 0 /*track version*/) chg
To keep things simple, I assume all your tracking tables have single-column PK.
You can find out tracking tables and their PKs by -
select c.table_name
,k.column_name pk
from sys.change_tracking_tables t
,information_schema.table_constraints c
,information_schema.key_column_usage k
where object_name(t.object_id) = c.table_name
and c.constraint_type = 'PRIMARY KEY'
and c.table_name = k.table_name
and c.constraint_name = k.constraint_name
Run CHANGETABLE for each tracking table and combine the results.
Here is the whole modified script -
set nocount on;
-- We want to check for changes since the previous version
--declare #prevTrackingVersion int = INSERT_YOUR_PREV_VERSION_HERE
-- Comment out this line if you know the previous version
declare #prevTrackingVersion int CHANGE_TRACKING_CURRENT_VERSION() - 1
-- Get a list of table with change tracking enabled
declare #trackedTables as table (table_name nvarchar(100), pk nvarchar(100));
insert into #trackedTables (table_name, pk)
select c.table_name
,k.column_name pk
from sys.change_tracking_tables t
,information_schema.table_constraints c
,information_schema.key_column_usage k
where object_name(t.object_id) = c.table_name
and c.constraint_type = 'PRIMARY KEY'
and c.table_name = k.table_name
and c.constraint_name = k.constraint_name
-- This will be the list of changes
declare #changes as table (table_name varchar(100), pk varchar(100))
-- For each table name in tracked tables
declare #table_name nvarchar(100)
,#pk nvarchar(100)
while exists(select top 1 * from #trackedTables)
begin
-- Set the current table name
select top 1 #table_name = table_name, #pk = pk from #trackedTables order by table_name asc;
insert into #changes (table_name, pk)
exec ('select ''' + #table_name + ''', ' + #pk + ' from CHANGETABLE(CHANGES ' + #table_name + ', ' + #prevTrackingVersion + ') chg')
-- Delete the current table name
delete from #trackedTables where table_name = #table_name;
end
select * from #changes;
You could create a trigger on each table in your schema (or rather the one you truly need) like this
CREATE TRIGGER TRX_TableName_ChangeLog ON TableName
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
DECLARE #TABLE_NAME NVARCHAR(400)
SELECT #TABLE_NAME = OBJECT_NAME(parent_object_id)
FROM sys.objects
WHERE sys.objects.object_id = ##PROCID
INSERT INTO ChangeTableLog (TABLE_NAME, TABLE_ID)
SELECT #TABLE_NAME,INSERTED.ID
FROM INSERTED --or delete
END
The creation of the trigger could be dynamically something like this:
DECLARE #TABLE_NAME nvarchar(200),#Trigger nvarchar(max)
DECLARE CURS CURSOR LOCAL FAST_FORWARD FOR
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
OPEN CURS
FETCH NEXT FROM CURS INTO #TABLE_NAME
WHILE ##FETCH_STATUS = 0
BEGIN
SET #Trigger =
'CREATE TRIGGER TRX_'+#TABLE_NAME+'_ChangeLog
ON '+#TABLE_NAME+'
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
DECLARE #TABLE_NAME NVARCHAR(400)
SELECT #TABLE_NAME = OBJECT_NAME(parent_object_id)
FROM sys.objects
WHERE sys.objects.object_id = ##PROCID
INSERT INTO ChangeTableLog (TABLE_NAME, TABLE_ID)
SELECT #TABLE_NAME,INSERTED.ID
FROM INSERTED
END'
EXEC(#Trigger)
FETCH NEXT FROM CURS INTO #TABLE_NAME
END
CLOSE CURS
DEALLOCATE CURS
Personally, I dislike the use of triggers but there is no doubt they are easy to implement. I don't know the scale of your system, but the best scenario is that the service which makes the entries will also include a new column in each table to keep changes or just state the date of execution per entry.
I have to check for value existence in a subset of tables in a subset of databases of a sql server instance. Beware I need to do this because I have 30 databases with same schema name and similar structure. Querying all databases separately is a waste of time.
The query generates correctly code for existing tables, but the additional check for column existence in table fails.
The column in some tables does not exist so the generated code must not include queries on tables without this column.
To solve this I need to realiably find a way to join sys.databases with sys.tables and then sys.columns. Or an alternative way to query all the required databases in a time saving manner.
SET NOCOUNT ON;
IF OBJECT_ID (N'tempdb.dbo.#temp') IS NOT NULL
DROP TABLE #temp
CREATE TABLE #temp
(
exist INT
, DB VARCHAR(50)
, tbname VARCHAR(500)
)
/*tables common root,
all tables i need to query start with this prefix and a number between 1 and 50
and some resulting tables do not exist
ex: dbo.Z_WBL_ASCHEDA23 exist in wbcto, while dbo.Z_WBL_ASCHEDA23 does not exist in db wbgtg
*/
DECLARE #TableName NVARCHAR(200)
SELECT #TableName = 'dbo.Z_WBL_ASCHEDA'
DECLARE #SQL NVARCHAR(MAX)
;WITH n(n) AS
(
SELECT 1
UNION ALL
SELECT n+1 FROM n WHERE n < 50
)
SELECT #SQL = STUFF((
SELECT CHAR(13)+'SELECT COUNT(1), ''' + db.name + ''', '''+
#TableName+CONVERT(VARCHAR, n.n)+''' FROM ' +#TableName+CONVERT(VARCHAR, n.n)
+ ' WHERE COALESCE(s_dettagli,'''') = ''CONTROLLATO'' '
+CHAR(13)
FROM sys.databases db
INNER JOIN n ON 1=1
INNER JOIN sys.tables t ON OBJECT_ID(db.name + '.' + #TableName+CONVERT(VARCHAR, n.n)) IS NOT NULL
INNER JOIN sys.columns c ON t.OBJECT_ID = c.OBJECT_ID and c.name = 's_dettagli'
/*join on columns not working, generates sql for tables without 's_dettagli' column and query fails*/
WHERE db.name like 'wb%' --check only databases starting with 'wb'
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
select #SQL
INSERT INTO #temp (exist, DB, tbname)
EXEC sys.sp_executesql #SQL
SELECT *
FROM #temp t
where exist <> 0
EDIT: adding some sql generated from query
SELECT COUNT(1), 'wb360', 'dbo.Z_WBL_ASCHEDA23' FROM wb360.dbo.Z_WBL_ASCHEDA23 WHERE COALESCE(s_dettagli,'') = 'CONTROLLATO'
SELECT COUNT(1), 'Wbbim', 'dbo.Z_WBL_ASCHEDA32' FROM Wbbim.dbo.Z_WBL_ASCHEDA32 WHERE COALESCE(s_dettagli,'') = 'CONTROLLATO'
the table of first query doesn't contain 's_dettagli' column
EDIT2: SOLUTION
EXEC sp_MSforeachdb '
IF ''?'' not like ''wb%''
RETURN
USE [?]
EXEC sp_MSforeachtable
#replacechar = ''!'',
#command1 = ''SELECT ''''?'''' AS db_name, ''''!'''' AS table_name, COUNT(*) FROM ! '',
#whereand = '' And Object_id In (
Select t.Object_id
From sys.objects t
INNER JOIN sys.columns c on c.Object_id = t.Object_id
Where t.name like ''''Z_WBL_ASCHEDA%''''
AND c.name = ''''s_dettagli'''' )'' '
Sys.columns can be joined to sys.tables using the object_id field (the object_id is the representation of the table itself).
sys.tables is run in the context of the database you are querying, hence you cannot see a table contained in another database. sys.databases can be run on any database on an instance and allow you to view other databases on the same instance. As such you don't need to join the table to the database (also the reason why there is no database_id field within sys.tables).
I hope that helps. Any clarification please let me know.
I would suggest alternative ways:
use registered Servers in SSMS and run the script on each database here
use exec sys.sp_MSforeachdb here
use sqlcmd and powershell to switch databases
I believe this script can help you :
SET NOCOUNT ON;
IF OBJECT_ID (N'tempdb.dbo.#Temp') IS NOT NULL
DROP TABLE #Temp
CREATE TABLE #Temp
(
exist INT
, DB VARCHAR(50)
, tbname VARCHAR(500)
)
DECLARE #SchemaName NVARCHAR(200)
DECLARE #TableName NVARCHAR(200)
DECLARE #ColumnName NVARCHAR(200)
DECLARE #SearchText NVARCHAR(200)
DECLARE #DBNameStartWith NVARCHAR(200)
DECLARE #SQL NVARCHAR(MAX)
SET #DBNameStartWith = 'wb'
SET #SchemaName = 'dbo'
SET #TableName = 'Z_WBL_ASCHEDA'
SET #ColumnName = 's_dettagli'
SET #SearchText = 'CONTROLLATO'
DECLARE #DatabaseName varchar(100)
DECLARE Crsr CURSOR FOR
SELECT name
FROM MASTER.sys.sysdatabases
WHERE name LIKE ''+#DBNameStartWith+'%'
OPEN Crsr
FETCH NEXT FROM Crsr INTO #DatabaseName
WHILE ##FETCH_STATUS = 0
BEGIN
IF ISNULL((SELECT COUNT(1) FROM SYS.TABLES T,SYS.COLUMNS C WHERE T.object_id=C.object_id AND T.name=#TableName AND C.name=#ColumnName),0)>0
BEGIN
SET #SQL = '
IF EXISTS (SELECT 1 FROM '+#DatabaseName+'.SYS.TABLES T,'+#DatabaseName+'.SYS.COLUMNS C WHERE T.object_id=C.object_id AND T.name='''+#TableName+''' AND C.name='''+#ColumnName+''')
BEGIN
SELECT COUNT(1),'''+#DatabaseName+''','''+#TableName+'''
FROM '+#DatabaseName+'.'+#SchemaName+'.'+#TableName+'
WHERE '+#ColumnName+'=''' +#SearchText+'''
END'
PRINT(#SQL)
INSERT INTO #Temp
EXEC sp_executesql #SQL
END
FETCH NEXT FROM Crsr INTO #DatabaseName
END
CLOSE Crsr
DEALLOCATE Crsr
SELECT * FROM #Temp
We're going to be going through a period of testing on a product soon. This product is a web application with a SQL Server 2008R2 backend.
Our database has several schemas within it (Customer, DataEntry, and a few others).
I have found ways to wipe all data in a database without breaking referential integrity or the data structures, which is close to what we're looking to do. The problem I'm finding is that we actually need a bunch of the data from some of the tables. Essentially, we only want to wipe the Customers schema.
We have a script written which will load in the test data for customers, but is there a way to change the techniques in my linked article to target only a specific schema? Is there a better way to clear all data in a schema?
A common scenario for me as well. I usually write what I call a reset script, deleting all data form the target tables in the order necessary to prevent referential errors, and then reseed the primary keys.
DELETE FROM < table 1 >
DELETE FROM < table 2 >
... etc ...
DBCC CHECKIDENT (< table 1 >, RESEED, 0)
DBCC CHECKIDENT (< table 2 >, RESEED, 0)
... etc ...
EDIT
To more fully answer the original question. to leave data in specific tables you would need to modify the block of code that does the deleting / truncating, and also modify the code that reseeds the idents in a similar way.
EXEC sp_MSForEachTable '
IF object_id(''?'') != < table name > AND object_id(''?'') != < table name > AND ... etc ...
BEGIN
IF OBJECTPROPERTY(object_id(''?''), ''TableHasForeignRef'') = 1
DELETE FROM ?
ELSE
TRUNCATE TABLE ?
END
'
GO
Just set the #schemaID to the name of the schema you wish to blow away and it should do the rest. If you end up with a FK dependency loop it will break and tell you what to do...
Declare #schemaID Nvarchar(256)
Set #schemaID = 'Schema' -- Set this to the name of the schema you wish to blow away
If Object_ID('tempdb..#tables') Is Not Null Drop Table #tables
Create Table #tables (tID Int, SchemaName Nvarchar(256), TableName Nvarchar(256))
Insert #tables
Select Row_Number() Over (Order By s.name, so.name), s.name, so.name
From sysobjects so
Join sys.schemas s
On so.uid = s.schema_id
Where so.xtype = 'u'
And s.name = #schemaID
Declare #SQL Nvarchar(Max),
#schema Nvarchar(256),
#table Nvarchar(256),
#iter Int = 1,
#loopCatch Int = 0
While Exists (Select 1
From #tables)
Begin
Select #schema = SchemaName,
#table = TableName
From #tables
Where tID = #iter
If Exists (Select 1
From sysobjects o
Join sys.schemas s1
On o.uid = s1.schema_id
Join sysforeignkeys fk
On o.id = fk.rkeyid
Join sysobjects o2
On fk.fkeyid = o2.id
Join sys.schemas s2
On o2.uid = s2.schema_id
Join #tables t
On o2.name = t.TableName Collate Database_Default
And s2.name = t.SchemaName Collate Database_Default
Where o.name = #table
And s1.name = #schema)
Begin
Update t
Set tID = (Select Max(tID) From #tables) + 1
From #tables t
Where tableName = #table
And schemaName = #schema
Set #iter = #iter + 1
End
Else
Begin
Set #Sql = 'Truncate Table [' + #schema + '].[' + #table + ']'
Begin Try
Exec sp_executeSQL #SQL;
Delete t
From #tables t
Where tableName = #table
And schemaName = #schema
Set #iter = #iter + 1
End Try
Begin Catch
Print #SQL
Update t
Set tID = (Select Max(tID) From #tables) + 1
From #tables t
Where tableName = #table
And schemaName = #schema
Set #iter = #iter + 1
Set #loopCatch = #loopCatch + 1;
If #loopCatch > 5
Begin
Select 'WARNING: Endless FK redundancy loop. Drop the constraints and these tables, truncate and reapply constraints manually'
Union All
Select '[' + SchemaName + '].[' + TableName + ']'
From #tables;
Break;
End
End Catch
End
End
This is parameterized on database and schema. If no schema is supplied, it will clear all data in the specified database.
Handles tables with foreign key references appropriately by disabling constraints. If the procedure fails, which it shouldn't normally do, ensure that you run it successfully after fixing the cause of the problem, which should ensure constraint checking goes back to normal.
This will not handle foreign key references correctly if you have foreign keys between schemas, however, it could be fairly easily amended to handle this.
create procedure [removeData] (#database_name sysname, #schema_name sysname = null)
as
set nocount on
create table #tables (
TableName varchar(900) not null primary key,
HasFKRef bit not null
);
declare #sql nvarchar(4000),
#table_name varchar(900);
if (db_id(#database_name) is null)
raiserror ('You must at least specify the database name', 16, 1);
set #sql = 'select ''['' + TABLE_CATALOG + ''].['' + TABLE_SCHEMA + ''].['' + TABLE_NAME + '']'' as TableName, (case when exists(select * from [' + #database_name + '].INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc inner join [' + #database_name + '].INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc on rc.UNIQUE_CONSTRAINT_CATALOG = tc.CONSTRAINT_CATALOG and rc.UNIQUE_CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA and rc.UNIQUE_CONSTRAINT_NAME = tc.CONSTRAINT_NAME where tc.TABLE_NAME = t.TABLE_NAME) then 1 else 0 end) as HasFKRef
from [' + #database_name + '].INFORMATION_SCHEMA.TABLES t
where TABLE_TYPE = ''BASE TABLE'' and TABLE_SCHEMA = isnull(#schema_name, TABLE_SCHEMA)';
insert into #tables
exec sp_executesql #sql, N'#schema_name sysname', #schema_name;
declare #curse cursor
set #curse = cursor fast_forward for
select sql from (
select 'alter table ' + TableName + ' nocheck constraint all' as sql, 1 as sort
from #tables
union all
select 'truncate table ' + TableName, 2 as sort
from #tables
where HasFKRef = 0
union all
select 'delete from ' + TableName, 3 as sort
from #tables
where HasFKRef = 1
union all
select 'alter table ' + TableName + ' with check check constraint all', 4 as sort
from #tables
) t
order by sort, sql
open #curse
fetch next from #curse into #sql
while (##fetch_status = 0)
begin
exec (#sql)
fetch next from #curse into #sql
end
close #curse
GO
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?
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'
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
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
remove the original Tbl_A_MyFK constraint (without the ON DELETE CASCADE)
ALTER TABLE Tbl_A DROP CONSTRAINT Tbl_A_MyFK
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
Here you can do your delete
DELETE FROM Tbl_A WHERE ...
drop your constraint Tbl_A_MyFK
ALTER TABLE Tbl_A DROP CONSTRAINT Tbl_A_MyFK
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)
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.
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
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:
select id's of records from the main table that I want to delete into a temp table
write a delete query for each related table which joins to the temp table.
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'''
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