Add Primary Key constraint automatically - sql

I have the following table InfoSchema which contains the SchemaName and the TableName of each table in my test database :
SchemaName TableName
dbo Employee
dbo Department
Function Company
Finance Payslips
Sub ProjectSub
I want to add for each table the constraint PrimaryKey to the column ending with ID or Id :
In dbo.Employee there is one column EmployeeId so the query will be like below :
ALTER TABLE dbo.Employee
ADD CONSTRAINT Employee_pk PRIMARY KEY (EmployeeId);
For Sub.ProjectSub there are 3 columns ending with Id :
ProjectId
CompanyId
SubId
The constraint will be added at the first column appearing in the structure of the table.

As I mention in my comment, you can use a dynamic statement to create the statements. I very strongly suggest looking over the SQL generated, however, so I do not include an EXEC sp_executesql statement here. PRINT or SELECT the value of #SQL and check it over first, then run the statements as you need:
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SET #SQL = STUFF((SELECT #CRLF + #CRLF +
N'ALTER TABLE ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + #CRLF +
N'ADD CONSTRAINT ' + QUOTENAME(CONCAT(t.[name],N'_PK')) + N' PRIMARY KEY (' + QUOTENAME(c.[name]) + N');'
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
CROSS APPLY (SELECT TOP 1 *
FROM sys.columns c
WHERE c.object_id = t.object_id
AND c.name LIKE '%id'
ORDER BY c.column_id ASC) c
WHERE NOT EXISTS (SELECT 1
FROM sys.key_constraints k
WHERE k.[type] = 'PK'
AND k.parent_object_id = t.object_id)
FOR XML PATH(N''),TYPE).value('.','nvarchar(MAX)'),1,4,N'');
PRINT #SQL;
This assumes that the first column, ordinally, needs to be the PK, and it will not attempt to create a PK on a table that already has one.

Not possible. There is no automatism for this in SQL Server, so it will require at least a script to run over the db to identify tables and columns and issue modify statements. Which is not "automatic" as it will not RUN automatic - you need to run it in a second step.

Related

T-SQL :: TRUNCATE or DELETE all tables in schema

I need to TRUNCATE or DELETE all tables in schema.
I found this code:
-- disable all constraints
EXEC sp_MSForEachTable #command1='ALTER TABLE ? NOCHECK CONSTRAINT all',#whereand='and Schema_Id=Schema_id(''Person'')'
-- delete data in all tables
Exec Sp_msforeachtable #command1='Truncate Table ?',#whereand='and Schema_Id=Schema_id(''Person'')'
-- enable all constraints
exec sp_MSForEachTable #command1='ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all',#whereand='and Schema_Id=Schema_id(''Person'')'
-- if some of the tables have identity columns we may want to reseed them
EXEC sp_MSForEachTable #command1='DBCC CHECKIDENT ( ''?'', RESEED, 0)',#whereand='and Schema_Id=Schema_id(''Person'')'
but on AdventureWorks it gives me:
Cannot truncate table 'Person.Address' because it is being referenced by a FOREIGN KEY constraint.
So I found this alternative code:
DECLARE #STRSQL NVARCHAR(MAX);
DECLARE #TABLE NVARCHAR(128);
DECLARE #SCHEMA_NAME VARCHAR(50)
SET #SCHEMA_NAME = 'Person'
SET #STRSQL = '';
DECLARE #C1 CURSOR SET #C1 = CURSOR
FOR
SELECT TOP 2 TABLE_SCHEMA + '.' + TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = #SCHEMA_NAME
OPEN #C1
FETCH NEXT
FROM #C1
INTO #TABLE
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT #TABLE
SET #STRSQL = #STRSQL + 'DELETE FROM ' + #TABLE + ';'
FETCH NEXT
FROM #C1
INTO #TABLE
END
CLOSE #C1
DEALLOCATE #C1
PRINT #STRSQL
EXEC sp_executesql #STRSQL
But the result is the same:
Person.Address
Person.AddressType
DELETE FROM Person.Address;DELETE FROM Person.AddressType;
Msg 547, Level 16, State 0, Line 1
The DELETE statement conflicted with the REFERENCE constraint "FK_BusinessEntityAddress_Address_AddressID". The conflict occurred in database "AdventureWorks2014", table "Person.BusinessEntityAddress", column 'AddressID'.
The statement has been terminated.
Msg 547, Level 16, State 0, Line 1
The DELETE statement conflicted with the REFERENCE constraint "FK_BusinessEntityAddress_AddressType_AddressTypeID". The conflict occurred in database "AdventureWorks2014", table "Person.BusinessEntityAddress", column 'AddressTypeID'.
The statement has been terminated.
How to TRUNCATE or DELETE all tables in schema?
You simply need to wrap your "delete from all the tables" script with a "drop all foreign keys" script at the beginning, and "re-create all foreign keys" script at the end. I show one way to do that here:
Drop and Re-Create All Foreign Key Constraints in SQL Server
However, I would argue that it is much cleaner to just script out the database and empty objects from source control than spend all this time and effort deleting data from one table at a time.
Anyway an attempt at what you're doing (if you truncate you don't also need to checkident, I'm not sure I would ever use undocumented and unsupported procedures like sp_msforeachtable, and I also stay the heck away from INFORMATION_SCHEMA). Please try this on a test database.
CREATE TABLE #x -- feel free to use a permanent table
(
drop_script nvarchar(max),
create_script nvarchar(max)
);
DECLARE #drop nvarchar(max) = N'',
#create nvarchar(max) = N'';
-- drop is easy, just build a simple concatenated list from sys.foreign_keys:
SELECT #drop += N'
ALTER TABLE ' + QUOTENAME(cs.name) + '.' + QUOTENAME(ct.name)
+ ' DROP CONSTRAINT ' + QUOTENAME(fk.name) + ';'
FROM sys.foreign_keys AS fk
INNER JOIN sys.tables AS ct
ON fk.parent_object_id = ct.[object_id]
INNER JOIN sys.schemas AS cs
ON ct.[schema_id] = cs.[schema_id];
INSERT #x(drop_script) SELECT #drop;
-- create is a little more complex. We need to generate the list of
-- columns on both sides of the constraint, even though in most cases
-- there is only one column.
SELECT #create += N'
ALTER TABLE '
+ QUOTENAME(cs.name) + '.' + QUOTENAME(ct.name)
+ ' ADD CONSTRAINT ' + QUOTENAME(fk.name)
+ ' FOREIGN KEY (' + STUFF((SELECT ',' + QUOTENAME(c.name)
-- get all the columns in the constraint table
FROM sys.columns AS c
INNER JOIN sys.foreign_key_columns AS fkc
ON fkc.parent_column_id = c.column_id
AND fkc.parent_object_id = c.[object_id]
WHERE fkc.constraint_object_id = fk.[object_id]
ORDER BY fkc.constraint_column_id
FOR XML PATH(N''), TYPE).value(N'./text()[1]', N'nvarchar(max)'), 1, 1, N'')
+ ') REFERENCES ' + QUOTENAME(rs.name) + '.' + QUOTENAME(rt.name)
+ '(' + STUFF((SELECT ',' + QUOTENAME(c.name)
-- get all the referenced columns
FROM sys.columns AS c
INNER JOIN sys.foreign_key_columns AS fkc
ON fkc.referenced_column_id = c.column_id
AND fkc.referenced_object_id = c.[object_id]
WHERE fkc.constraint_object_id = fk.[object_id]
ORDER BY fkc.constraint_column_id
FOR XML PATH(N''), TYPE).value(N'./text()[1]', N'nvarchar(max)'), 1, 1, N'') + ');'
FROM sys.foreign_keys AS fk
INNER JOIN sys.tables AS rt -- referenced table
ON fk.referenced_object_id = rt.[object_id]
INNER JOIN sys.schemas AS rs
ON rt.[schema_id] = rs.[schema_id]
INNER JOIN sys.tables AS ct -- constraint table
ON fk.parent_object_id = ct.[object_id]
INNER JOIN sys.schemas AS cs
ON ct.[schema_id] = cs.[schema_id]
WHERE rt.is_ms_shipped = 0 AND ct.is_ms_shipped = 0;
UPDATE #x SET create_script = #create;
PRINT #drop;
PRINT #create;
EXEC sys.sp_executesql #drop
-- clear out data etc. here
DECLARE #truncate nvarchar(max) = N'';
SELECT #truncate += N'TRUNCATE TABLE ' + QUOTENAME(s.name)
+ N'.' + QUOTENAME(t.name) + N';'
FROM sys.schemas AS s
INNER JOIN sys.tables AS t
ON s.[schema_id] = t.[schema_id];
EXEC sys.sp_executesql #truncate;
EXEC sys.sp_executesql #create;
Notes:
this is untested. As ludly as I can: try this on a test database.
this was meant to execute exactly once, so I don't drop the #temp table (it may be useful to keep it alive long enough to troubleshoot if things go south)
PRINT is not necessarily going to show you the full command that is going to be executed, so it's not a valid way to determine if the script is correct. It is just meant as a quick eyeball. If you really want to view the whole command, you'll need something a little more elaborate.
this doesn't handle indexed views, and I'm sure there are other limitations that might prevent you from truncating some tables (I'm thinking temporal or always encrypted with enclaves or in-mem), but I would resolve those separately and keep truncate around instead of "fixing" that by using a much more log-intensive delete.
Thank you #AaronBertrand, I forked your awesome query to make a more suitable one for what I need to do:
SELECT cs.name AS SchemaName
,ct.name AS TableName
,rt.name AS ColumnName
,fk.name AS ForeignKeyName
,fk.object_id AS ObjectID
,fk.parent_object_id AS ParentObjectID
,
-- drop constraint
N'
ALTER TABLE ' + QUOTENAME(cs.name) + '.' + QUOTENAME(ct.name) + ' DROP CONSTRAINT ' + QUOTENAME(fk.name) + ';' AS Drop_Constraint_Script
,
-- create constraint
N'
ALTER TABLE ' + QUOTENAME(cs.name) + '.' + QUOTENAME(ct.name) + ' ADD CONSTRAINT ' + QUOTENAME(fk.name) + ' FOREIGN KEY (' + STUFF((
SELECT ',' + QUOTENAME(c.name)
-- get all the columns in the constraint table
FROM sys.columns AS c
INNER JOIN sys.foreign_key_columns AS fkc ON fkc.parent_column_id = c.column_id
AND fkc.parent_object_id = c.[object_id]
WHERE fkc.constraint_object_id = fk.[object_id]
ORDER BY fkc.constraint_column_id
FOR XML PATH(N'')
,TYPE
).value(N'.[1]', N'nvarchar(max)'), 1, 1, N'') + ') REFERENCES ' + QUOTENAME(rs.name) + '.' + QUOTENAME(rt.name) + '(' + STUFF((
SELECT ',' + QUOTENAME(c.name)
-- get all the referenced columns
FROM sys.columns AS c
INNER JOIN sys.foreign_key_columns AS fkc ON fkc.referenced_column_id = c.column_id
AND fkc.referenced_object_id = c.[object_id]
WHERE fkc.constraint_object_id = fk.[object_id]
ORDER BY fkc.constraint_column_id
FOR XML PATH(N'')
,TYPE
).value(N'.[1]', N'nvarchar(max)'), 1, 1, N'') + ');' AS Create_Constraint_Script
FROM sys.foreign_keys AS fk
INNER JOIN sys.tables AS rt -- referenced table
ON fk.referenced_object_id = rt.[object_id]
INNER JOIN sys.schemas AS rs ON rt.[schema_id] = rs.[schema_id]
INNER JOIN sys.tables AS ct -- constraint table
ON fk.parent_object_id = ct.[object_id]
INNER JOIN sys.schemas AS cs ON ct.[schema_id] = cs.[schema_id]
WHERE rt.is_ms_shipped = 0
AND ct.is_ms_shipped = 0
ORDER BY 1
,2
,3

Dynamic SQL Server Query loop through schema find primary key duplicate

EDIT: There seems to be some confusion around the create table statements. These are there solely as a demonstration of what tables *might come in to our synapse instance, not as actual code that will run. The important part of the question is contained in the latter half.
I am trying to create a stored procedure that loops through every table in a supplied schema and outputs the count of duplicate primary key rows for each table. Assume that the data is being supplied from elsewhere and the primary keys are not being enforced. For example I may have three tables in the stack schema:
CREATE TABLE stack.table1(
id int,
name NVARCHAR(MAX),
color NVARCHAR(20)
PRIMARY KEY (id))
INSERT INTO stack.table1 VALUES(1,'item1','yellow')
(2,'item2','blue')
(2,'item2','blue')
CREATE TABLE stack.table2(
id int,
name NVARCHAR(MAX),
size NVARCHAR(1)
PRIMARY KEY (id,size))
INSERT INTO stack.table2 VALUES(1,'item1','L')
(2,'item2','M')
(3,'item2','S')
CREATE TABLE stack.table3(
id int,
name NVARCHAR(MAX),
weight NVARCHAR(20)
PRIMARY KEY (id))
INSERT INTO stack.table1 VALUES(1,'item1','200lb')
(2,'item2','150lb')
(3,'item2','125lb')
I want to supply a variable to a stored procedure to indicate the schema (in this case 'stack') and have that procedure spit out a table with the names of the tables in the schema and the counts of duplicate primary key rows. So in this example a stored procedure called 'loopcheck' would look like this:
Query:
EXEC loopcheck #schema = 'stack'
Output:
table
duplicate_count
table1
1
table2
0
table3
0
I am using an Azure Synapse instance so there are several functions that are not available (such as FOR XML PATH and others.) Since each table may have a single column primary key or a composite primary key I need to join to the system provided tables to get primary key info. My general idea was like so:
CREATE procedure loopcheck #schema= NVARCHAR(MAX)
AS
BEGIN
create table #primarykey(
SCHEMA_NAME nvarchar(400),
TABLE_NAME nvarchar(500),
COLUMN_NAME nvarchar(500)
)
insert into #primarykey
select l.TABLE_SCHEMA,
l.TABLE_NAME,
l.COLUMN_NAME
from INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE l
inner join INFORMATION_SCHEMA.TABLE_CONSTRAINTS t on l.constraint_Name = t.CONSTRAINT_NAME
where
l.table_schema = #schema
CREATE TABLE #groupBy2(
TABLE_NAME nvarchar(50),
groupby nvarchar(200)
)
INSERT INTO #groupBy2
SELECT TABLE_NAME, STRING_AGG(CONVERT(NVARCHAR(max), COLUMN_NAME), ',') as groupby
FROM #primarykey
GROUP BY TABLE_NAME
DECLARE #currentTable NVARCHAR(MAX)=''
DECLARE #currentGroup NVARCHAR(MAX)=''
create table #work4(
TABLE_NAME nvarchar(400),
COUNT int)
DECLARE #final NVARCHAR(MAX)=
'INSERT INTO #work4
SELECT '+#currentTable+', COUNT(*) FROM '+#currentTable+'GROUP BY'+#currentGroup
WHILE (SELECT COUNT(*) FROM #groupby2)>0
BEGIN
SET #currentTable =(SELECT TOP 1 TABLE_NAME FROM #groupby2 ORDER BY TABLE_NAME)
SET #currentGroup =(SELECT TOP 1 groupby FROM #groupby2 ORDER BY TABLE_NAME)
exec #final
DELETE #groupby2 where TABLE_NAME =#currentTable
END
END
This code gives me an error:
Incorrect syntax near 'SELECT'
but doesn't give me the actual line it has the error on.
Your primary issue was syntax errors: parameter declaration should not have = between name and type name, and missing spaces in the dynamic SQL.
Also
A schema name (or any object) can be up to nvarchar(128), you can use the alias sysname
You don't need to do any loops or use temp tables, you can build one big dynamic statement to execute
CREATE procedure loopcheck
#schema sysname
AS
DECLARE #sql nvarchar(max) = (
SELECT STRING_AGG(CAST('
SELECT
TableName = ' + QUOTENAME(t.name, '''') + ',
IndexName = ' + QUOTENAME(i.name, '''') + ',
Duplicates = COUNT(*)
FROM (
SELECT 1 n
FROM ' + QUOTENAME(s.name) + '.' + QUOTENAME(t.name) + ' t
GROUP BY ' + cols.agg + '
HAVING COUNT(*) > 1
) t
'
AS nvarchar(max)), 'UNION ALL')
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
AND s.name = #schema
JOIN sys.indexes i ON i.object_id = t.object_id
AND i.is_unique = 1
CROSS APPLY (
SELECT STRING_AGG('t.' + QUOTENAME(c.name), ', ')
FROM sys.index_columns ic
JOIN sys.columns c ON c.column_id = ic.column_id AND c.object_id = t.object_id
WHERE ic.index_id = i.index_id
AND ic.object_id = i.object_id
AND ic.is_included_column = 0
) cols(agg)
);
PRINT #sql; -- for testing
EXEC sp_executesql #sql;
db<>fiddle
I feel like there might be a slightly more efficient method using GROUPING SETS in cases where there are multiple unique indexes/constraints on a single table, but I'll leave that to you.

SQL Server column date default gatedate() to datetime in scripts

Note> I can not modify old scripts!!!
First script create table TABLENAME.
I have table TABLENAME with column COLUMNNAME DATE DEFAULT GETDATE();
I need to do: ->
ALTER TABLE TABLENAME
ALTER COLUMN COLUMNNAME DATETIME;
I get a error:
The object 'DF__TABLENAME__COLUMNNAME__7BC8385B' is dependent on column 'COLUMNNAME'.
I have this scripts on multiple databases so this part 'DF__TABLENAME__COLUMNNAME__7BC8385B'
is on every database different.
I can make
ALTER TABLE TABLENAME
DROP CONSTRAINT DF__TABLENAME__COLUMNNAME__7BC8385B;
And it will work, but It will not be very effective use it manually on all DB, I need make the script which will alter this column on everywhere without needed to know that constraint name.
If you've got variable constraint names you'll have to generate the change script per database. If you've got faith in this you can put the resulting script into an sp_executesql call.
This is using the sys views to query the database structure and generate the script in the last result column.
declare #TableName nvarchar(128) = 'TABLENAME';
declare #ColumnName nvarchar(128) = 'COLUMNNAME';
SELECT t.name [table], c.name [column], typ.name [type]
, def.name [DefaultConstraint], def.definition [DefaultValue]
, 'ALTER TABLE [' + t.name + '] DROP CONSTRAINT [' + def.name + ']; ALTER TABLE [' + t.name + '] ALTER COLUMN [' + c.name + '] DATETIME; ALTER TABLE [' + t.name + '] ADD CONSTRAINT [DF_' + t.name + '_' + c.name + '] DEFAULT getdate() FOR [' + c.name + '];'
FROM sys.columns c
JOIN sys.tables t ON c.object_id = t.object_id
JOIN sys.types typ on c.user_type_id=typ.user_type_id
LEFT JOIN sys.default_constraints def on c.default_object_id=def.object_id
WHERE typ.name = 'date'
and t.name = #TableName and c.name = #ColumnName
order by t.name, c.name

Return a count of all foreign keys across all tables in database without a cursor

I need to return a count of a particular foreign key across all tables in a database given a table name, column name, and an id.
So if the passed in parameters are TableName, ColumnName, RecordID, I would need to look at all tables that have that column as a foreign key and return the count of all RecordID's that match.
I can see how to do it with a cursor and dynamic sql, but I'd like to figure out something a bit more elegant as it were.
I guess I really just need someone smarter than me to tell me you can or can't do it.
EDIT
Output should be something like:
Count of ID 1 in OtherTable is 5
Count of ID 1 in OtherTable2 is 10
So you could pass in any table name and record ID and have it return counts for each referenced table
EDIT 2. I think there should be a better answer than this, but here's what I came up with. It does use dynamic sql, but no cursors at least. (It uses adventureworks)
DECLARE #RecID AS INT = 1
DECLARE #TableName AS VARCHAR(255) = 'Product'
CREATE TABLE #temp
(
tablename VARCHAR(255),
ColumnName VARCHAR(255),
sqlStatment VARCHAR(max),
IDCount INT
)
INSERT INTO #temp
(tablename,
ColumnName,
sqlStatment)
SELECT Object_schema_name(f.parent_object_id)
+ '.' + Object_name(f.parent_object_id) AS TableName,
Col_name(fc.parent_object_id, fc.parent_column_id) AS ColumnName,
'update #Temp set IDCount = (select count(*) from '
+ Object_schema_name(f.parent_object_id)
+ '.' + Object_name(f.parent_object_id)
+ ' where '
+ Col_name(fc.parent_object_id, fc.parent_column_id)
+ ' = ' + CONVERT(VARCHAR, #RecID)
+ ') where tablename = '''
+ Object_schema_name(f.parent_object_id)
+ '.' + Object_name(f.parent_object_id) + ''';'
FROM sys.foreign_keys AS f
INNER JOIN sys.foreign_key_columns AS fc
ON f.OBJECT_ID = fc.constraint_object_id
WHERE Object_name(f.referenced_object_id) = #TableName
DECLARE #sql AS VARCHAR(max) = ''
SELECT #sql = #Sql + sqlStatment
FROM #temp
EXEC (#sql)
SELECT *
FROM #temp
DROP TABLE #temp
I think this is what you want :
SELECT COUNT(*)
FROM sys.foreign_key_columns AS fk
JOIN sys.tables AS t ON fk.parent_object_id = t.object_id
JOIN sys.columns AS c ON fk.parent_object_id = c.object_id AND fk.parent_column_id = c.column_id
WHERE fk.referenced_object_id = (SELECT object_id FROM sys.tables WHERE name = 'TableThatContainsTheKey');
I hope this helps:
use yourdatabase
select count(*) from sysobjects where xtype='F'
(Edit: sorry, I meant 'F'. 'PK' is for primary keys.)

Diff / Delta script: ideas on streamlining it?

I'm pulling data from a remote DB into a local MS SQL Server DB, the criteria being whether the PK is higher than I have in my data warehouse or whether the edit date is within a range that I provide with an argument. That works super fast so I am happy with it. However, when I attempt to sync this delta table into my data warehouse it takes quite a long time.
Here's my SPROC:
ALTER PROCEDURE [dbo].[sp_Sync_Delta_Table]
#tableName varchar(max)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sql as varchar(4000)
-- Delete rows in MIRROR database where ID exists in the DELTA database
SET #sql = 'Delete from [SERVER-WHS].[MIRROR].[dbo].[' + #tableName
+ '] Where [ID] in (Select [ID] from [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + '])'
EXEC(#sql)
-- Insert all deltas
SET #sql = 'Insert Into [SERVER-WHS].[MIRROR].[dbo].[' + #tableName
+ '] Select * from [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + ']'
EXEC(#sql)
END
It works, but I think it takes way too long. For example: inserting 3590 records from the DELTA table into the MIRROR table containing 3,600,761 took over 25 minutes.
Can anyone give me a hint on how I can make this job easier on SSMS? I'm using 2008 R2, btw.
Thanks again!
Nate
The issue is likely the time required to do a table scan on the 3,600,761 to see if the new records are unique.
First of all, let's confirm that the primary key (ID) on the target table is the clustered index and increasing.
SELECT s.name, o.name, i.name, i.type_desc, ic.key_ordinal, c.name
FROM sys.objects o
JOIN sys.columns c ON (c.object_id = o.object_id)
JOIN sys.schemas s ON (s.schema_id = o.schema_id)
JOIN sys.indexes i ON (i.object_id = o.object_id)
JOIN sys.index_columns ic ON (ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.column_id = c.column_id)
WHERE o.name = '[table_name]'
If the index is not an ascending integer, it is possible that the inserts are causing lots of page splits.
Second, what other objects does that insert affect. Are there triggers, materialized views, or non-clustered indexes?
Third, do you have
My suggestion would be to stage the data on the mirror server in a local table. It can be as simple as as:
SET #sql = 'SELECT INTO * [MIRROR].[dbo].[' + #tableName + '_Staging] from [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + ']'
EXEC(#sql)
After that add a clustered primary key to the table.
SET #sql = 'ALTER TABLE [MIRROR].[dbo].[' + #tableName + '_Staging] ADD CONSTRAINT [PK_' + #tableName + '] PRIMARY KEY CLUSTERED (Id ASC)'
EXEC(#sql)
At this point, try inserting the data into the real table. The optimizer should be much more helpful this time.
Change the delete portion to:
SET #sql = 'Delete tbl1 from [SERVER-WHS].[MIRROR].[dbo].[' + #tableName
+ '] tbl1 inner join [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + '] tbl2 on tbl1.[ID] = tbl2.[ID]'
In future use INNER JOIN instead of IN with Sub Query.