/* ============================================================ Compare Script (Schema + Rowcounts + Programmable Objects) SQL Server 2022 Compares SOURCE and TARGET databases without comparing row contents: - Rowcount per table (diff list + summary) - Tables/columns (datatype/length/nullable/collation) diff list + summary - Programmable objects (views/procs/functions) via SHA2_256 hash of definition - Triggers via SHA2_256 hash of definition - Users/Roles existence + role memberships (name-based) Output: - Textual progress and summary via RAISERROR ... WITH NOWAIT - Result sets with detailed diffs Configure DB names below. ============================================================ */ SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @SourceDb sysname = N'sysdb_UTF8'; DECLARE @TargetDb sysname = N'sysdb_utf8_jr'; DECLARE @QSource sysname = QUOTENAME(@SourceDb); DECLARE @QTarget sysname = QUOTENAME(@TargetDb); DECLARE @sev int = 10; DECLARE @Now nvarchar(30); DECLARE @msg nvarchar(4000); SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); SET @msg = CONCAT(@Now, N' | COMPARE_START | Source=', @SourceDb, N' | Target=', @TargetDb); RAISERROR(@msg, @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 1) Rowcounts per table -------------------------------------------------------------------------------- SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | STEP 1/5 | Rowcounts per table...'), @sev, 1) WITH NOWAIT; IF OBJECT_ID('tempdb..#RowCounts') IS NOT NULL DROP TABLE #RowCounts; CREATE TABLE #RowCounts( DbName sysname NOT NULL, SchemaName sysname NOT NULL, TableName sysname NOT NULL, RowCount bigint NOT NULL ); DECLARE @sql nvarchar(max); SET @sql = N' INSERT INTO #RowCounts(DbName, SchemaName, TableName, RowCount) SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', s.name, t.name, SUM(p.rows) FROM ' + @QSource + N'.sys.tables t JOIN ' + @QSource + N'.sys.schemas s ON s.schema_id = t.schema_id JOIN ' + @QSource + N'.sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1) WHERE t.is_ms_shipped=0 GROUP BY s.name, t.name;'; EXEC sp_executesql @sql; SET @sql = N' INSERT INTO #RowCounts(DbName, SchemaName, TableName, RowCount) SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', s.name, t.name, SUM(p.rows) FROM ' + @QTarget + N'.sys.tables t JOIN ' + @QTarget + N'.sys.schemas s ON s.schema_id = t.schema_id JOIN ' + @QTarget + N'.sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1) WHERE t.is_ms_shipped=0 GROUP BY s.name, t.name;'; EXEC sp_executesql @sql; ;WITH s AS ( SELECT SchemaName, TableName, RowCount FROM #RowCounts WHERE DbName=@SourceDb ), t AS ( SELECT SchemaName, TableName, RowCount FROM #RowCounts WHERE DbName=@TargetDb ), d AS ( SELECT COALESCE(s.SchemaName, t.SchemaName) AS SchemaName, COALESCE(s.TableName, t.TableName) AS TableName, s.RowCount AS SourceRows, t.RowCount AS TargetRows, (ISNULL(t.RowCount,0) - ISNULL(s.RowCount,0)) AS Diff FROM s FULL OUTER JOIN t ON t.SchemaName = s.SchemaName AND t.TableName = s.TableName ) SELECT * INTO #RowDiff FROM d WHERE ISNULL(SourceRows,-1) <> ISNULL(TargetRows,-1); DECLARE @RowDiffCount int = (SELECT COUNT(*) FROM #RowDiff); DECLARE @RowTotalSource int = (SELECT COUNT(*) FROM (SELECT DISTINCT SchemaName, TableName FROM #RowCounts WHERE DbName=@SourceDb) x); DECLARE @RowTotalTarget int = (SELECT COUNT(*) FROM (SELECT DISTINCT SchemaName, TableName FROM #RowCounts WHERE DbName=@TargetDb) x); SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | ROWCOUNTS | SourceTables=', @RowTotalSource, N' TargetTables=', @RowTotalTarget, N' TablesWithRowDiff=', @RowDiffCount), @sev, 1) WITH NOWAIT; IF @RowDiffCount = 0 RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROWCOUNTS_OK | All table rowcounts match.'), @sev, 1) WITH NOWAIT; ELSE BEGIN RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROWCOUNTS_DIFF | Showing tables with rowcount differences...'), @sev, 1) WITH NOWAIT; SELECT * FROM #RowDiff ORDER BY ABS(Diff) DESC, SchemaName, TableName; END -------------------------------------------------------------------------------- -- 2) Column/schema diffs -------------------------------------------------------------------------------- SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | STEP 2/5 | Table/column schema diffs...'), @sev, 1) WITH NOWAIT; IF OBJECT_ID('tempdb..#Cols') IS NOT NULL DROP TABLE #Cols; CREATE TABLE #Cols( DbName sysname, SchemaName sysname, TableName sysname, ColumnName sysname, TypeName sysname, MaxLen int, PrecisionVal int, ScaleVal int, IsNullable bit, CollationName sysname NULL ); SET @sql = N' INSERT #Cols SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', s.name, t.name, c.name, ty.name, c.max_length, c.precision, c.scale, c.is_nullable, c.collation_name FROM ' + @QSource + N'.sys.columns c JOIN ' + @QSource + N'.sys.tables t ON t.object_id=c.object_id JOIN ' + @QSource + N'.sys.schemas s ON s.schema_id=t.schema_id JOIN ' + @QSource + N'.sys.types ty ON ty.user_type_id=c.user_type_id WHERE t.is_ms_shipped=0;'; EXEC sp_executesql @sql; SET @sql = N' INSERT #Cols SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', s.name, t.name, c.name, ty.name, c.max_length, c.precision, c.scale, c.is_nullable, c.collation_name FROM ' + @QTarget + N'.sys.columns c JOIN ' + @QTarget + N'.sys.tables t ON t.object_id=c.object_id JOIN ' + @QTarget + N'.sys.schemas s ON s.schema_id=t.schema_id JOIN ' + @QTarget + N'.sys.types ty ON ty.user_type_id=c.user_type_id WHERE t.is_ms_shipped=0;'; EXEC sp_executesql @sql; ;WITH s AS (SELECT * FROM #Cols WHERE DbName=@SourceDb), t AS (SELECT * FROM #Cols WHERE DbName=@TargetDb), d AS ( SELECT COALESCE(s.SchemaName, t.SchemaName) AS SchemaName, COALESCE(s.TableName, t.TableName) AS TableName, COALESCE(s.ColumnName, t.ColumnName) AS ColumnName, CONCAT(ISNULL(s.TypeName,N''), N'(', ISNULL(CONVERT(nvarchar(20),s.MaxLen),N''), N')') AS SourceType, CONCAT(ISNULL(t.TypeName,N''), N'(', ISNULL(CONVERT(nvarchar(20),t.MaxLen),N''), N')') AS TargetType, s.IsNullable AS SourceNullable, t.IsNullable AS TargetNullable, s.CollationName AS SourceCollation, t.CollationName AS TargetCollation FROM s FULL OUTER JOIN t ON t.SchemaName = s.SchemaName AND t.TableName = s.TableName AND t.ColumnName = s.ColumnName ) SELECT * INTO #ColDiff FROM d WHERE ISNULL(SourceType,N'') <> ISNULL(TargetType,N'') OR ISNULL(SourceNullable,2) <> ISNULL(TargetNullable,2) OR ISNULL(SourceCollation,N'') <> ISNULL(TargetCollation,N''); DECLARE @ColDiffCount int = (SELECT COUNT(*) FROM #ColDiff); RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COLUMNS | ColumnDiffs=', @ColDiffCount), @sev, 1) WITH NOWAIT; IF @ColDiffCount = 0 RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COLUMNS_OK | No column definition diffs detected.'), @sev, 1) WITH NOWAIT; ELSE BEGIN RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COLUMNS_DIFF | Showing column diffs...'), @sev, 1) WITH NOWAIT; SELECT * FROM #ColDiff ORDER BY SchemaName, TableName, ColumnName; END -------------------------------------------------------------------------------- -- 3) Programmable objects -------------------------------------------------------------------------------- RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | STEP 3/5 | Programmable objects (hash compare)...'), @sev, 1) WITH NOWAIT; IF OBJECT_ID('tempdb..#Modules') IS NOT NULL DROP TABLE #Modules; CREATE TABLE #Modules( DbName sysname, ObjectType nvarchar(60), SchemaName sysname, ObjectName sysname, DefinitionHash varbinary(32) NULL ); SET @sql = N' INSERT #Modules SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', o.type_desc, s.name, o.name, HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition)) FROM ' + @QSource + N'.sys.objects o JOIN ' + @QSource + N'.sys.schemas s ON s.schema_id=o.schema_id JOIN ' + @QSource + N'.sys.sql_modules m ON m.object_id=o.object_id WHERE o.is_ms_shipped=0 AND o.type IN (''V'',''P'',''FN'',''IF'',''TF'');'; EXEC sp_executesql @sql; SET @sql = N' INSERT #Modules SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', o.type_desc, s.name, o.name, HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition)) FROM ' + @QTarget + N'.sys.objects o JOIN ' + @QTarget + N'.sys.schemas s ON s.schema_id=o.schema_id JOIN ' + @QTarget + N'.sys.sql_modules m ON m.object_id=o.object_id WHERE o.is_ms_shipped=0 AND o.type IN (''V'',''P'',''FN'',''IF'',''TF'');'; EXEC sp_executesql @sql; ;WITH s AS (SELECT * FROM #Modules WHERE DbName=@SourceDb), t AS (SELECT * FROM #Modules WHERE DbName=@TargetDb), d AS ( SELECT COALESCE(s.ObjectType, t.ObjectType) AS ObjectType, COALESCE(s.SchemaName, t.SchemaName) AS SchemaName, COALESCE(s.ObjectName, t.ObjectName) AS ObjectName, CASE WHEN s.ObjectName IS NULL THEN 'MISSING_IN_SOURCE' WHEN t.ObjectName IS NULL THEN 'MISSING_IN_TARGET' WHEN s.DefinitionHash <> t.DefinitionHash THEN 'DIFFERENT_DEFINITION' ELSE 'OK' END AS Status FROM s FULL OUTER JOIN t ON t.ObjectType = s.ObjectType AND t.SchemaName = s.SchemaName AND t.ObjectName = s.ObjectName ) SELECT * INTO #ModuleDiff FROM d WHERE Status <> 'OK'; DECLARE @ModuleDiffCount int = (SELECT COUNT(*) FROM #ModuleDiff); RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | MODULES | Differences=', @ModuleDiffCount), @sev, 1) WITH NOWAIT; IF @ModuleDiffCount = 0 RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | MODULES_OK | No module definition diffs detected.'), @sev, 1) WITH NOWAIT; ELSE BEGIN RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | MODULES_DIFF | Showing module diffs...'), @sev, 1) WITH NOWAIT; SELECT * FROM #ModuleDiff ORDER BY ObjectType, SchemaName, ObjectName; END -------------------------------------------------------------------------------- -- 4) Triggers -------------------------------------------------------------------------------- RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | STEP 4/5 | Triggers (hash compare)...'), @sev, 1) WITH NOWAIT; IF OBJECT_ID('tempdb..#Triggers') IS NOT NULL DROP TABLE #Triggers; CREATE TABLE #Triggers( DbName sysname, TriggerSchema sysname, TriggerName sysname, ParentSchema sysname, ParentName sysname, DefinitionHash varbinary(32) NULL ); SET @sql = N' INSERT #Triggers SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', ss.name, tr.name, ps.name, pt.name, HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition)) FROM ' + @QSource + N'.sys.triggers tr JOIN ' + @QSource + N'.sys.tables pt ON pt.object_id = tr.parent_id JOIN ' + @QSource + N'.sys.schemas ps ON ps.schema_id = pt.schema_id JOIN ' + @QSource + N'.sys.schemas ss ON ss.schema_id = tr.schema_id JOIN ' + @QSource + N'.sys.sql_modules m ON m.object_id = tr.object_id WHERE tr.is_ms_shipped=0;'; EXEC sp_executesql @sql; SET @sql = N' INSERT #Triggers SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', ss.name, tr.name, ps.name, pt.name, HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition)) FROM ' + @QTarget + N'.sys.triggers tr JOIN ' + @QTarget + N'.sys.tables pt ON pt.object_id = tr.parent_id JOIN ' + @QTarget + N'.sys.schemas ps ON ps.schema_id = pt.schema_id JOIN ' + @QTarget + N'.sys.schemas ss ON ss.schema_id = tr.schema_id JOIN ' + @QTarget + N'.sys.sql_modules m ON m.object_id = tr.object_id WHERE tr.is_ms_shipped=0;'; EXEC sp_executesql @sql; ;WITH s AS (SELECT * FROM #Triggers WHERE DbName=@SourceDb), t AS (SELECT * FROM #Triggers WHERE DbName=@TargetDb), d AS ( SELECT COALESCE(s.TriggerSchema, t.TriggerSchema) AS TriggerSchema, COALESCE(s.TriggerName, t.TriggerName) AS TriggerName, COALESCE(s.ParentSchema, t.ParentSchema) AS ParentSchema, COALESCE(s.ParentName, t.ParentName) AS ParentTable, CASE WHEN s.TriggerName IS NULL THEN 'MISSING_IN_SOURCE' WHEN t.TriggerName IS NULL THEN 'MISSING_IN_TARGET' WHEN s.DefinitionHash <> t.DefinitionHash THEN 'DIFFERENT_DEFINITION' ELSE 'OK' END AS Status FROM s FULL OUTER JOIN t ON t.TriggerSchema = s.TriggerSchema AND t.TriggerName = s.TriggerName AND t.ParentSchema = s.ParentSchema AND t.ParentName = s.ParentName ) SELECT * INTO #TriggerDiff FROM d WHERE Status <> 'OK'; DECLARE @TriggerDiffCount int = (SELECT COUNT(*) FROM #TriggerDiff); RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | TRIGGERS | Differences=', @TriggerDiffCount), @sev, 1) WITH NOWAIT; IF @TriggerDiffCount = 0 RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | TRIGGERS_OK | No trigger definition diffs detected.'), @sev, 1) WITH NOWAIT; ELSE BEGIN RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | TRIGGERS_DIFF | Showing trigger diffs...'), @sev, 1) WITH NOWAIT; SELECT * FROM #TriggerDiff ORDER BY TriggerSchema, TriggerName, ParentSchema, ParentTable; END -------------------------------------------------------------------------------- -- 5) Users/Roles -------------------------------------------------------------------------------- RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | STEP 5/5 | Users/Roles (existence + memberships)...'), @sev, 1) WITH NOWAIT; IF OBJECT_ID('tempdb..#Principals') IS NOT NULL DROP TABLE #Principals; CREATE TABLE #Principals( DbName sysname, Name sysname, TypeDesc nvarchar(60) ); SET @sql = N' INSERT #Principals SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', name, type_desc FROM ' + @QSource + N'.sys.database_principals WHERE type IN (''S'',''U'',''G'',''R'') AND name NOT IN (''dbo'',''guest'',''INFORMATION_SCHEMA'',''sys'');'; EXEC sp_executesql @sql; SET @sql = N' INSERT #Principals SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', name, type_desc FROM ' + @QTarget + N'.sys.database_principals WHERE type IN (''S'',''U'',''G'',''R'') AND name NOT IN (''dbo'',''guest'',''INFORMATION_SCHEMA'',''sys'');'; EXEC sp_executesql @sql; ;WITH s AS (SELECT * FROM #Principals WHERE DbName=@SourceDb), t AS (SELECT * FROM #Principals WHERE DbName=@TargetDb), d AS ( SELECT COALESCE(s.Name, t.Name) AS PrincipalName, s.TypeDesc AS SourceType, t.TypeDesc AS TargetType, CASE WHEN s.Name IS NULL THEN 'MISSING_IN_SOURCE' WHEN t.Name IS NULL THEN 'MISSING_IN_TARGET' WHEN s.TypeDesc <> t.TypeDesc THEN 'TYPE_DIFF' ELSE 'OK' END AS Status FROM s FULL OUTER JOIN t ON t.Name = s.Name ) SELECT * INTO #PrincipalDiff FROM d WHERE Status <> 'OK'; DECLARE @PrincipalDiffCount int = (SELECT COUNT(*) FROM #PrincipalDiff); RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | PRINCIPALS | Differences=', @PrincipalDiffCount), @sev, 1) WITH NOWAIT; IF @PrincipalDiffCount > 0 BEGIN RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | PRINCIPALS_DIFF | Showing principal diffs...'), @sev, 1) WITH NOWAIT; SELECT * FROM #PrincipalDiff ORDER BY PrincipalName; END ELSE RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | PRINCIPALS_OK | No principal diffs detected.'), @sev, 1) WITH NOWAIT; IF OBJECT_ID('tempdb..#RoleMembers') IS NOT NULL DROP TABLE #RoleMembers; CREATE TABLE #RoleMembers( DbName sysname, RoleName sysname, MemberName sysname ); SET @sql = N' INSERT #RoleMembers SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', r.name AS RoleName, m.name AS MemberName FROM ' + @QSource + N'.sys.database_role_members rm JOIN ' + @QSource + N'.sys.database_principals r ON r.principal_id = rm.role_principal_id JOIN ' + @QSource + N'.sys.database_principals m ON m.principal_id = rm.member_principal_id WHERE r.name NOT IN (''public'');'; EXEC sp_executesql @sql; SET @sql = N' INSERT #RoleMembers SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', r.name AS RoleName, m.name AS MemberName FROM ' + @QTarget + N'.sys.database_role_members rm JOIN ' + @QTarget + N'.sys.database_principals r ON r.principal_id = rm.role_principal_id JOIN ' + @QTarget + N'.sys.database_principals m ON m.principal_id = rm.member_principal_id WHERE r.name NOT IN (''public'');'; EXEC sp_executesql @sql; ;WITH s AS (SELECT RoleName, MemberName FROM #RoleMembers WHERE DbName=@SourceDb), t AS (SELECT RoleName, MemberName FROM #RoleMembers WHERE DbName=@TargetDb), d AS ( SELECT COALESCE(s.RoleName, t.RoleName) AS RoleName, COALESCE(s.MemberName, t.MemberName) AS MemberName, CASE WHEN s.RoleName IS NULL THEN 'MISSING_IN_SOURCE' WHEN t.RoleName IS NULL THEN 'MISSING_IN_TARGET' ELSE 'OK' END AS Status FROM s FULL OUTER JOIN t ON t.RoleName = s.RoleName AND t.MemberName = s.MemberName ) SELECT * INTO #RoleMemberDiff FROM d WHERE Status <> 'OK'; DECLARE @RoleMemberDiffCount int = (SELECT COUNT(*) FROM #RoleMemberDiff); RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROLEMEMBERS | Differences=', @RoleMemberDiffCount), @sev, 1) WITH NOWAIT; IF @RoleMemberDiffCount > 0 BEGIN RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROLEMEMBERS_DIFF | Showing role membership diffs...'), @sev, 1) WITH NOWAIT; SELECT * FROM #RoleMemberDiff ORDER BY RoleName, MemberName; END ELSE RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROLEMEMBERS_OK | No role membership diffs detected.'), @sev, 1) WITH NOWAIT; RAISERROR(CONCAT( CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COMPARE_DONE | ', N'RowDiffTables=', @RowDiffCount, N' | ColumnDiffs=', @ColDiffCount, N' | ModuleDiffs=', @ModuleDiffCount, N' | TriggerDiffs=', @TriggerDiffCount, N' | PrincipalDiffs=', @PrincipalDiffCount, N' | RoleMemberDiffs=', @RoleMemberDiffCount ), @sev, 1) WITH NOWAIT;