create a table with table and database statistics - sql-server

There are around 200+ tables in our SQL DB and every table has 1 common field [updated_timestamp]
Is there a way to query the DB itself and list all tables, with the MAX value of [updated_timestamp] and the row count of each table?
i'm sorry if i've not explained that the best
are there secret/system tables that hold such info?
my desired output would be
table
updated_timestamp
row_count
Table A
2022-08-22
89,854
Table B
2022-08-18
103,55,166
if there is a table like this i could intergate that would be great, but i'm assuming not that simple.
i picked up some code from another stored procedure and was hoping this would be of use for the row count at least
SELECT
QUOTENAME(SCHEMA_NAME(sOBJ.schema_id)) AS [DB_Schema],
QUOTENAME(sOBJ.name) AS [TableName],
SUM(sPTN.Rows) AS [Row_Count]
INTO ##tmpRowCount2
FROM
sys.objects AS sOBJ
INNER JOIN sys.partitions AS sPTN
ON sOBJ.object_id = sPTN.object_id
WHERE
sOBJ.type = 'U'
AND sOBJ.is_ms_shipped = 0x0
AND index_id < 2
GROUP BY
sOBJ.schema_id
, sOBJ.name
ORDER BY [Row_Count]
GO
ALTER TABLE ##tmpRowCount2 ADD updated_timestamp datetime NULL;
-- keep only API rows
DELETE FROM ##tmpRowCount2
WHERE [DB_Schema] != '[api]'
DECLARE #Row_Count int
DECLARE #sql nvarchar(max)
DECLARE #TableName as VARCHAR(256)
DECLARE #DB_Schema as VARCHAR(256)
DECLARE #updated_timestamp as DATETIME
DECLARE tablenamefromcursor CURSOR FOR
SELECT TableName,
Row_Count,
DB_Schema
FROM ##tmpRowCount2
OPEN tablenamefromcursor
FETCH NEXT FROM tablenamefromcursor INTO #TableName, #Row_Count, #DB_Schema
WHILE ##FETCH_STATUS = 0
BEGIN
SET #sql = 'UPDATE ##tmpRowCount2 SET updated_timestamp = ' +
'(SELECT MAX([updated_timestamp]) FROM ' + #DB_Schema + '.' + #TableName +
') WHERE TableName = ''' + #TableName + ''''
EXEC(#sql)
FETCH NEXT FROM tablenamefromcursor INTO #TableName, #Row_Count, #DB_Schema
END
CLOSE tablenamefromcursor
DEALLOCATE tablenamefromcursor

Code editied above and i can confirm it works, by debugging with select #sql iwas able to parse the statement and edit the syntax unitl it worked by getting the parethesis and quotes in the right order

Related

SQL Server dynamic SQL to query on sys tables

I'm using SQL Server 2012 and have written a stored procedure that calculates table statistics using the sys tables. I realize there are reports in SSMS that do this for you, however, I'm wanting additional information. It seems like I'm forced to use dynamic SQL to make this happen, but it seems slow and clunky. My stored procedure works, but I was curious there a more efficient way to write the query, maybe even without dynamic SQL?
DECLARE #my_cursor CURSOR,
#sql nvarchar(250), #table_name nvarchar(100),
#days_loaded int, #row_count int, #row_count_per_day int,
#used_mb numeric(36,2), #used_mb_per_day numeric(36,2),
#first_date_loaded date, #last_date_loaded date,
#expected_first_date date, #gaps_exist nvarchar(1)
-- Delete temp table #tables in case the stored procedure is ran twice in the same session
IF OBJECT_ID('tempdb.dbo.#tables', 'U') IS NOT NULL
DROP TABLE #tables
-- Select all the tables and stats from the dallas schema into a temp table
SELECT
s.name + '.' + t.name AS table_name,
p.rows AS row_count,
CAST(ROUND(SUM(a.used_pages) / 128.00, 2) AS NUMERIC(36, 2)) AS used_mb
INTO
#tables
FROM
sys.tables AS t
INNER JOIN
sys.indexes AS i ON t.object_id = i.object_id
INNER JOIN
sys.partitions AS p ON i.object_id = p.object_id AND i.index_id = p.index_id
INNER JOIN
sys.allocation_units AS a ON p.partition_id = a.container_id
INNER JOIN
sys.schemas AS s ON t.schema_id = s.schema_id
WHERE
s.name = 'dallas'
GROUP BY
t.name, s.name, p.rows
-- Add some additional fields in the temp table to be used for population in loop below
ALTER TABLE #tables ADD days_loaded INT NULL
ALTER TABLE #tables ADD used_mb_per_day numeric(36,2) NULL
ALTER TABLE #tables ADD row_count_per_day INT NULL
ALTER TABLE #tables ADD first_date_loaded DATE NULL
ALTER TABLE #tables ADD expected_first_date DATE NULL
ALTER TABLE #tables ADD gaps_exist nvarchar(1) NULL
-- Loop through each table in dallas schema
BEGIN
SET #my_cursor = CURSOR FOR SELECT table_name, row_count, used_mb FROM #tables
OPEN #my_cursor
FETCH NEXT FROM #my_cursor INTO #table_name, #row_count, #used_mb
WHILE ##FETCH_STATUS = 0
BEGIN
-- Calculate first date that was loaded to see history of how far back table goes
SET #sql = N'SELECT TOP 1 #first_date_loaded = CONVERT(date,Min(data_load_timestamp)) FROM ' + #table_name
EXECUTE sp_executesql #sql, N'#first_date_loaded date OUTPUT', #first_date_loaded=#first_date_loaded OUTPUT
-- Calculate last date that was loaded to offset expected first date if needed (data that is loaded day of instead of day before)
SET #sql = N'SELECT TOP 1 #last_date_loaded = CONVERT(date,Max(data_load_timestamp)) FROM ' + #table_name
EXECUTE sp_executesql #sql, N'#last_date_loaded date OUTPUT', #last_date_loaded=#last_date_loaded OUTPUT
-- Calculate days loaded by retrieving count of distinct dates in table
SET #sql = N'SELECT #days_loaded=COUNT(*) FROM (SELECT DISTINCT CONVERT(date, data_load_timestamp) AS data_load_date FROM ' + #table_name + ') tt'
EXECUTE sp_executesql #sql, N'#days_loaded int OUTPUT', #days_loaded=#days_loaded OUTPUT
IF #days_loaded <> 0
BEGIN
SET #row_count_per_day = #row_count / #days_loaded
SET #used_mb_per_day = #used_mb / #days_loaded
SET #expected_first_date = DATEADD(d,#days_loaded * -1,GETDATE())
IF #last_date_loaded = CONVERT(date,GETDATE()) SET #expected_first_date = DATEADD(d,1,#expected_first_date)
IF #first_date_loaded = #expected_first_date
SET #gaps_exist = 'N'
ELSE
SET #gaps_exist = 'Y'
END
-- Update temp table with additional fields that were calculated
SET #sql = N'UPDATE #tables SET days_loaded=' + CONVERT(nvarchar,#days_loaded) +
', row_count_per_day=' + CONVERT(nvarchar, #row_count_per_day) +
', used_mb_per_day=' + CONVERT(nvarchar,#used_mb_per_day) + ', first_date_loaded=''' +
CONVERT(nvarchar,#first_date_loaded) + ''', expected_first_date=''' +
CONVERT(nvarchar,#expected_first_date) + ''', gaps_exist=''' + #gaps_exist + ''' WHERE table_name=''' + #table_name + ''''
EXECUTE sp_executesql #sql
FETCH NEXT FROM #my_cursor INTO #table_name, #row_count, #used_mb
END
CLOSE #my_cursor
DEALLOCATE #my_cursor
END
-- Final return showing results from temp table sorted by largest utilization first
SELECT * FROM #tables ORDER BY used_mb DESC

How can I list all databases and all tables of my databases with a final "Attach" and list total row of each table in SQL Server?

I did this. But unfortunately that return all in many table. I want to return all in one unique table. Maybe using "UNION" but I don't know the way to do.
This is my code:
EXEC sp_msforeachdb 'select ''?''AS "DataBase", s.name, t.name AS "Tables",max(si.rows) as "Rows Line"
from [?].sys.tables t inner join [?].sys.schemas s
on t.schema_id = s.schema_id
inner join [?].sys.partitions si on t.object_id = si.object_id
where t.name like "%ATTACH" group by s.name,t.name'`
You cannot do it in a single query.
You could query the sys.databases table to get a temporary table of all your databases, and then run a dynamic query on each database to store the results of the query in your question all in another temporary table.
Then at the end, you just select all rows from the last temporary table.
I have found finally a solution.i just used a Stored Procedures for having the result i was looking for.So decided to post the answer here maybe that will help someone else.
DECLARE #banco_nome nvarchar(MAX), #tabela_nome nvarchar(MAX)
DECLARE #banco_cursor CURSOR
DECLARE #sqlstatement nvarchar(MAX)
DECLARE #count_sql nvarchar(MAX)
DECLARE #total int
DECLARE #RegistrosFotograficos TABLE
(
DatabaseName nvarchar(max),
TableName nvarchar(max),
Total int
)
SET #banco_cursor = CURSOR FORWARD_ONLY FOR
SELECT name FROM sys.databases
OPEN #banco_cursor
FETCH NEXT FROM #banco_cursor INTO #banco_nome
WHILE ##FETCH_STATUS = 0
BEGIN
SET #sqlstatement = 'DECLARE tabela_cursor CURSOR FORWARD_ONLY FOR SELECT TABLE_NAME FROM ' + #banco_nome + '.INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ''BASE TABLE'' AND TABLE_NAME LIKE ''%ATTACH'' ORDER BY TABLE_NAME'
EXEC sp_executesql #sqlstatement
OPEN tabela_cursor
FETCH NEXT FROM tabela_cursor INTO #tabela_nome
WHILE ##FETCH_STATUS = 0
BEGIN
SET #count_sql = 'USE ' + #banco_nome + '; SELECT #total=COUNT(1) FROM ' + #tabela_nome;
EXECUTE sp_executesql #count_sql, N'#total int OUTPUT', #total=#total OUTPUT
INSERT INTO #RegistrosFotograficos (DatabaseName, TableName, Total) VALUES (#banco_nome, #tabela_nome, #total);
FETCH NEXT FROM tabela_cursor INTO #tabela_nome
END
CLOSE tabela_cursor;
DEALLOCATE tabela_cursor;
FETCH NEXT FROM #banco_cursor INTO #banco_nome
END
CLOSE #banco_cursor;
DEALLOCATE #banco_cursor;
SELECT * FROM #RegistrosFotograficos
This will list all the tables in a given database and the number of rows in each. Notice the results are in a table named #results:
set nocount on
declare #curtable sysname
declare #prevtable sysname
declare #curcount int
declare #tsql varchar(500)
if object_ID('tempdb..#curtables','U') is not null
drop table #curtables
select name into #curtables
from sys.objects
where type='U'
order by 1
if object_id('tempdb..#results','U') is not null
drop table #results
create table #results(name sysname,numrows int)
select top 1 #curtable=name from #curtables order by name
while (1=1)
begin
set #tsql = 'select '''+quotename(#curtable) +''',count(*) numrows from '+quotename(#curtable)
print #tsql
insert into #results
exec (#tsql)
set #prevtable= #curtable
select top 1 #curtable = name
from #curtables
where name > #prevtable
order by name
if #curtable=#prevtable
break
end

TSQL Find MIN/MAX LEN ColName over many tables

I need to find the MAX LEN for all responses in column across many tables. Some of these column names match across tables, some don't. If they match, I need to use the column name through all the tables that it matches. So, say 10 tables with a total of 200 different column names. I created a TempCol table. I also have all the table names in a temp table. My thought is to add cols to the TempCol table for Table1MAX, Table1MIN, Table2MAX, Table2MIN etc.
This finds the MAX (IF Col_1 is in Table1):
SELECT MAX(LEN(Col_1)) FROM Table1
I'd like to fill in the TempCol table, with the values from above, but I keep hitting a wall.
Anybody have any ideas?
Here's one way, for the MAX length only; you can use it as a starting point and modify/extend it to include more information in the results table:
create table dbo.Results (
TableName sysname not null,
ColumnName sysname not null,
MaxLength int not null default 0,
primary key (TableName, ColumnName)
)
insert into dbo.Results (TableName, ColumnName)
select object_name(sc.object_id), sc.name
from sys.columns sc
join sys.types st
on sc.system_type_id = st.system_type_id
-- get data for varchar(max) columns only
where st.name = 'varchar' and sc.max_length = -1
declare #TableName sysname, #ColumnName sysname, #sql nvarchar(max)
declare TablesAndColumns cursor local fast_forward
for
select TableName, ColumnName
from dbo.Results
open TablesAndColumns
fetch next from TablesAndColumns into #TableName, #ColumnName
while ##fetch_status = 0
begin
set #sql = 'update dbo.Results set MaxLength = (select isnull(max(len(' + #ColumnName + ')),0) ' +
' from ' + #TableName + ') ' +
' where TableName = ''' + #TableName + ''' and ColumnName = ''' + #ColumnName + ''''
print #sql
exec sp_executesql #sql
fetch next from TablesAndColumns into #TableName, #ColumnName
end
close TablesAndColumns
deallocate TablesAndColumns
select * from dbo.Results
Using cursors and dynamic SQL isn't always a good idea, but for a one-time system task like this it's a reasonable approach.

Bulk modification of unique ids in SQL Server

[This is a bit of an unusual problem, I know...]
What I need is a script that will change every unique id value to new one in our database. The problem is that we have configuration tables that can be exported between instances of our software which is id-sensitive (clobbering existing ids). Years ago, we set up a "wide-enough" id gap between our development "standard configuration" and our client's instances, which is now not wide enough :( - e.g. we're getting id conflicts when clients import our standard configuration.
A SQL script to do the following is definitely the simplest/shortest-timeframe thing that we can do. e.g. fixing the code is far too complicated and error prone to consider. Note that we are not "eliminating" the problem here. Just changing the gap from 1000's to 1000000's or more (the existing gap took 5 years to fill).
I believe the simplest solution would be to:
change all our tables to UPDATE_CASCADE (none of them are - this will greatly simplify the script)
create an identity table with the new lowest id that we want
For each table, modify the id to the next one in the identity table (using identity insert modifier flags where necessary). Perhaps after each table is processed, we could reset the identity table.
turn off UPDATE_CASCADE, and delete the identity table.
I am seeking any (partial or full) scripts for this.
Unfortunately UPDATE_CASCADE doesn't exist in the world of Sql Server. I suggest for each table you to re-key you do the following (Pseudo Code)
BACKUP DATABASE
CHECK BACKUP WORKS!
FOR EACH TABLE TO BE RE-KEYED
DROP ALL FOREIGN KEY CONSTRAINTS, INDEXES ETC FROM TABLE
SELECT ID + Number, ALL_OTHER_FIELDS INTO TEMP_TABLE FROM TABLE
RENAME TABLE OLD_TABLE
RENAME TEMP_TABLE TABLE
FOR ALL TABLES REFERENCING THIS TABLE
UPDATE FOREIGN_KEY_TABLE SET FK_ID = FK_ID + new number
END FOR
RE-APPLY FOREIGN KEY CONSTRAINTS, INDEXES ETC FROM TABLE
END FOR
Check it all still works ...
This process could be automated through DMO/SMO objects, but depending on the number of tables involved I'd say using management studio to generate scripts that can then be edited is probably quicker. After all, you only need to do this once/5 years.
Here we go with the code for SQL 2005. It's huge, it's hacky, but it will work (except in the case where you have a primary key that is a composite of two other primary keys).
If someone can re-write this with MrTelly's faster id addition (which wouldn't require building sql from a cursor for each updated row), then I'll mark that as the accepted answer. (If I don't notice the new answer, upvote this - then I'll notice :))
BEGIN TRAN
SET NOCOUNT ON;
DECLARE #newLowId INT
SET #newLowId = 1000000
DECLARE #sql VARCHAR(4000)
--**** SELECT ALL TABLES WITH IDENTITY COLUMNS ****
DECLARE tables SCROLL CURSOR
FOR
SELECT '[' + SCHEMA_NAME(schema_id) + '].[' + t.name + ']', c.name
FROM sys.identity_columns c
INNER JOIN sys.objects t
on c.object_id = t.object_id
WHERE t.type_Desc = 'USER_TABLE'
OPEN tables
DECLARE #Table VARCHAR(100)
DECLARE #IdColumn VARCHAR(100)
CREATE Table #IdTable(
id INT IDENTITY(1,1),
s CHAR(1)
)
FETCH FIRST FROM tables
INTO #Table, #IdColumn
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT('
****************** '+#Table+' ******************
')
--Reset the idtable to the 'low' id mark - remove this line if you want all records to have distinct ids across the database
DELETE FROM #IdTable
DBCC CHECKIDENT('#IdTable', RESEED, #newLowId)
--**** GENERATE COLUMN SQL (for inserts and deletes - updating identities is not allowed) ****
DECLARE tableColumns CURSOR FOR
SELECT column_name FROM information_schema.columns
WHERE '[' + table_schema + '].[' + table_name + ']' = #Table
AND column_name <> #IdColumn
OPEN tableColumns
DECLARE #columnName VARCHAR(100)
DECLARE #columns VARCHAR(4000)
SET #columns = ''
FETCH NEXT FROM tableColumns INTO #columnName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #columns = #columns + #columnName
FETCH NEXT FROM tableColumns INTO #columnName
IF ##FETCH_STATUS = 0 SET #columns = #columns + ', '
END
CLOSE tableColumns
DEALLOCATE tableColumns
--**** GENERATE FOREIGN ROW UPDATE SQL ****
DECLARE foreignkeys SCROLL CURSOR
FOR
SELECT con.name,
'[' + SCHEMA_NAME(f.schema_id) + '].[' + f.name + ']' fTable, fc.column_name ,
'[' + SCHEMA_NAME(p.schema_id) + '].[' + p.name + ']' pTable, pc.column_name
FROM sys.foreign_keys con
INNER JOIN sysforeignkeys syscon
ON con.object_id = syscon.constid
INNER JOIN sys.objects f
ON con.parent_object_id = f.object_id
INNER JOIN information_schema.columns fc
ON fc.table_schema = SCHEMA_NAME(f.schema_id)
AND fc.table_name = f.name
AND fc.ordinal_position = syscon.fkey
INNER JOIN sys.objects p
ON con.referenced_object_id = p.object_id
INNER JOIN information_schema.columns pc
ON pc.table_schema = SCHEMA_NAME(p.schema_id)
AND pc.table_name = p.name
AND pc.ordinal_position = syscon.rkey
WHERE '[' + SCHEMA_NAME(p.schema_id) + '].[' + p.name + ']' = #Table
OPEN foreignkeys
DECLARE #FKeyName VARCHAR(100)
DECLARE #FTable VARCHAR(100)
DECLARE #FColumn VARCHAR(100)
DECLARE #PTable VARCHAR(100)
DECLARE #PColumn VARCHAR(100)
--**** RE-WRITE ALL IDS IN THE TABLE ****
SET #sql='DECLARE tablerows CURSOR FOR
SELECT CAST('+#IdColumn+' AS VARCHAR) FROM '+#Table+' ORDER BY '+#IdColumn
PRINT(#sql)
exec(#sql)
OPEN tablerows
DECLARE #rowid VARCHAR(100)
DECLARE #id VARCHAR(100)
FETCH NEXT FROM tablerows INTO #rowid
WHILE ##FETCH_STATUS = 0
BEGIN
--generate new id
INSERT INTO #IdTable VALUES ('')
SELECT #id = CAST(##IDENTITY AS VARCHAR)
IF #rowId <> #Id
BEGIN
PRINT('Modifying '+#Table+': changing '+#rowId+' to '+#id)
SET #sql='SET IDENTITY_INSERT ' + #Table + ' ON
INSERT INTO '+#Table+' ('+#IdColumn+','+#columns+') SELECT '+#id+','+#columns+' FROM '+#Table+' WHERE '+#IdColumn+'='+#rowId
--Updating all foreign rows...
FETCH FIRST FROM foreignkeys
INTO #FKeyName, #FTable, #FColumn, #PTable, #PColumn
WHILE ##FETCH_STATUS = 0
BEGIN
SET #sql = #sql + '
UPDATE '+#FTable+' SET '+#FColumn+'='+#id+' WHERE '+#FColumn+' ='+#rowId
FETCH NEXT FROM foreignkeys
INTO #FKeyName, #FTable, #FColumn, #PTable, #PColumn
END
SET #sql=#sql + '
DELETE FROM '+#Table+' WHERE '+#IdColumn+'='+#rowId
PRINT(#sql)
exec(#sql)
END
FETCH NEXT FROM tablerows INTO #rowid
END
CLOSE tablerows
DEALLOCATE tablerows
CLOSE foreignkeys
DEALLOCATE foreignkeys
--Revert to normal identity operation - update the identity to the latest id...
DBCC CHECKIDENT(#Table, RESEED, ##IDENTITY)
SET #sql='SET IDENTITY_INSERT ' + #Table + ' OFF'
PRINT(#sql)
exec(#sql)
FETCH NEXT FROM tables
INTO #Table, #IdColumn
END
CLOSE tables
DEALLOCATE tables
DROP TABLE #IdTable
--COMMIT
--ROLLBACK
Why don't you use negative numbers for your standard configuration values and continue to use positive numbers for other things?

In SQL Server 2005, can I do a cascade delete without setting the property on my tables?

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

Resources