{"id":602,"date":"2021-08-31T20:37:38","date_gmt":"2021-08-31T19:37:38","guid":{"rendered":"https:\/\/sqldoubleg.live-website.com\/?p=602"},"modified":"2021-09-05T09:08:45","modified_gmt":"2021-09-05T08:08:45","slug":"how-i-compared-1000s-of-databases-schemas-at-once","status":"publish","type":"post","link":"https:\/\/www.sqldoubleg.com\/es\/2021\/08\/31\/how-i-compared-1000s-of-databases-schemas-at-once\/","title":{"rendered":"C\u00f3mo comparar el esquema de miles de bases de datos"},"content":{"rendered":"\nCompare database schemas can be tricky, here I show you how I managed to compare thousands of databases&#8217; schemas at once&nbsp;\n\n\n\n<p>Working with databases is always fun and you never know what can be knocking on your door, even when you think everything is under control.<\/p>\n\n\n\n<p>As the title of this post states, today I will explain in detail how I managed to compare thousands of databases schemas at once, just by following the popular and well-known saying \u00ab<em>How to Eat an Elephant<\/em>\u00ab<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"549\" src=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_01-1-1024x549.jpg\" alt=\"\" class=\"wp-image-604\" srcset=\"https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_01-1-1024x549.jpg 1024w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_01-1-300x161.jpg 300w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_01-1-768x411.jpg 768w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_01-1-1536x823.jpg 1536w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_01-1-2048x1097.jpg 2048w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_01-1-150x80.jpg 150w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Background<\/h2>\n\n\n\n<p>When it comes to choosing the right architecture for your data, there is not \u00abone size fits all\u00bb solution, it depends on multiple factors and nowadays with more and more offerings from cloud providers, the number of possibilities has grown dramatically.<\/p>\n\n\n\n<p>This wasn&#8217;t the case not so long ago, when people used to put <em><strong>everything<\/strong><\/em> in the database, just because &#8230; just because.<\/p>\n\n\n\n<p>In those good old times, there were still choices to make, like for instance, to have one big database for your application or multiple databases sharing the same schema.<\/p>\n\n\n\n<p>In multi-tenant environments, having one database per customer <s>was<\/s> is very common, because it brings some benefits from the security and sometimes from the performance point of view.<\/p>\n\n\n\n<p>But also it comes with the challenge of keeping all databases schemas in sync.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Schema Comparison Tools<\/h2>\n\n\n\n<p>There are tools that can compare two databases in great detail, but the caveat I found, it is that they can <strong>only<\/strong> compare <strong>two<\/strong> databases, so while they&#8217;ll be fine to compare DEV&lt;=&gt;PROD or QA&lt;=&gt;PROD, they&#8217;re not a good fit to compare and find the differences in, literally, thousands of databases<\/p>\n\n\n\n<p>There it&#8217;s where I was, so after some time trying to postpone the inevitable, it was time to roll up my sleeves and get the job done.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">My Approach<\/h2>\n\n\n\n<p>My approach for a task like this is very simple, just like the saying <\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>\u00ab<strong><em>How to Eat an Elephant<\/em><\/strong>\u00ab, <em><strong>One Byte at a Time<\/strong><\/em><\/p><\/blockquote>\n\n\n\n<p>But in my case, one [database] object at time \ud83d\ude42<\/p>\n\n\n\n<p>First thing was to write the script that generates each object&#8217;s definition to be able to compare each other. And for simplicity, I also generate a hash for each object to compare, rather than the text.<\/p>\n\n\n\n<p>In my case (yours might be different) I wanted to compare all SQL modules (SP&#8217;s, UDF&#8217;s, TVF&#8217;s, triggers, views), all tables and user defined type tables, that gives me a good starting point.<\/p>\n\n\n\n<p>Thankfully I have written most parts of it at some point in the last 10 years, so I headed to <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/SQLDoubleG\/DBA-database\/tree\/master\/Scripts\" target=\"_blank\">my GitHub repo<\/a> and started to grab pieces from here and there.<\/p>\n\n\n\n<p>For the purpose of this post, I restored the lightest copy of AdventureWorks 100 times on 2 instances, plus one more that will be the source of truth. <br>They are named [AdventureWorksLT_001] to [AdventureWorksLT_200] and the master just [AdventureWorksLT]<\/p>\n\n\n\n<p>The TSQL script to generate all the objects I&#8217;m interested on, it looks like this:<\/p>\n\n\n\n<p><pre class=\"brush: tsql; title: ; notranslate\" title=\"\">SET NOCOUNT ON;\nDECLARE @object_name\tnvarchar(257) = '' \nDECLARE @sqlstring\t\tnvarchar(MAX)\nDECLARE @dbname\t\t\tsysname = 'AdventureWorksLT'\n\nSET @dbname = NULLIF(@dbname, '')\nSET @object_name = NULLIF(@object_name, '')\n\nIF OBJECT_ID('tempdb..#output') IS NOT NULL DROP TABLE #output\n\nCREATE TABLE #output  (\n    database_name sysname\t\t\tNOT NULL, \n    object_type nvarchar(60)\t\tNOT NULL, \n    object_name nvarchar(257)\t\tNOT NULL, \n    object_definition nvarchar(MAX) NOT NULL\n)\n\nDECLARE dbs CURSOR FORWARD_ONLY READ_ONLY FAST_FORWARD LOCAL\n\tFOR SELECT name \n            FROM sys.databases \n \t\t    WHERE database_id &amp;gt; 4 \n\t\t        AND state = 0\n\t\t        AND (name = @dbname OR (@dbname IS NULL AND name LIKE 'AdventureWorksLT_%'))\n\nOPEN dbs \nFETCH NEXT FROM dbs INTO @dbname \n\nWHILE @@FETCH_STATUS = 0 BEGIN \n\tSET @sqlstring\t= N'USE ' + QUOTENAME(@dbname) + N'\n\nSET NOCOUNT ON;\n\nSELECT DB_NAME() COLLATE DATABASE_DEFAULT AS [database_name] \n\t\t, o.type_desc COLLATE DATABASE_DEFAULT AS [object_type]\n\t\t, OBJECT_SCHEMA_NAME(o.object_id) + ''.'' + OBJECT_NAME(o.object_id) COLLATE DATABASE_DEFAULT AS [object_name] \n\t\t, LTRIM(RTRIM(OBJECT_DEFINITION(o.OBJECT_ID))) AS [object_definition]\n\tFROM sys.objects AS o\n\tWHERE o.type IN (''TR'',''FN'',''P'',''TF'',''V'')\n        AND (o.object_id = OBJECT_ID(@object_name) OR @object_name IS NULL)\n\nUNION ALL\n\nSELECT DB_NAME() COLLATE DATABASE_DEFAULT AS [database_name]\n\t\t, type_desc COLLATE DATABASE_DEFAULT AS [type_desc] \n\t\t, OBJECT_SCHEMA_NAME(object_id) + ''.'' + name COLLATE DATABASE_DEFAULT AS object_name\n\t\t, ''CREATE TABLE '' + QUOTENAME(OBJECT_SCHEMA_NAME(object_id)) + ''.'' + QUOTENAME(name) + ''('' + CHAR(10) +\n\t\t\t(STUFF(\n\t\t\t\t\t(SELECT '', '' + QUOTENAME(c.name) + '' '' + t.name + \n\t\t\t\t\t\t\tCASE WHEN t.name IN (''char'', ''nchar'', ''binay'', ''varchar'', ''nvarchar'', ''varbinay'')\n\t\t\t\t\t\t\t\tTHEN ''('' + CONVERT(SYSNAME, c.max_length) + '')'' \n\t\t\t\t\t\t\t\tELSE ''''\n\t\t\t\t\t\t\tEND +\n\t\t\t\t\t\t\tCASE WHEN c.is_identity = 1 THEN '' IDENTITY'' ELSE '''' END +\n\t\t\t\t\t\t\tISNULL('' COLLATE '' + c.collation_name, '''') +\n\t\t\t\t\t\t\tCASE WHEN c.is_nullable = 0 THEN '' NOT'' ELSE '''' END + '' NULL'' + CHAR(10)\n\t\t\t\t\t\tFROM sys.columns AS c\n\t\t\t\t\t\t\tINNER JOIN sys.types AS t\n\t\t\t\t\t\t\t\tON t.user_type_id = c.user_type_id\n\t\t\t\t\t\tWHERE c.object_id = o.object_id\n\t\t\t\t\t\tORDER BY column_id\n\t\t\t\t\t\tFOR XML PATH('''')), 1, 2, '''')) + '')'' COLLATE DATABASE_DEFAULT AS object_definition\n\tFROM sys.objects AS o\n\tWHERE type IN (''U'')\n\t\tAND OBJECTPROPERTY(object_id, ''IsMsShipped'') = 0\n\t\tAND (object_id = OBJECT_ID(@object_name) OR @object_name IS NULL)\n\nUNION ALL\n\nSELECT DB_NAME() COLLATE DATABASE_DEFAULT AS database_name \n\t\t, o.type_desc COLLATE DATABASE_DEFAULT\n\t\t, SCHEMA_NAME(tt.schema_id) + ''.'' + tt.name COLLATE DATABASE_DEFAULT AS object_name\n\t\t, ''CREATE TYPE '' + QUOTENAME(SCHEMA_NAME(tt.schema_id)) + ''.'' + QUOTENAME(tt.name) + '' AS TABLE ('' + CHAR(10) +\n\t\t\t(STUFF(\n\t\t\t\t\t(SELECT '', '' + QUOTENAME(c.name) + '' '' + t.name + \n\t\t\t\t\t\t\tCASE WHEN t.name IN (''char'', ''nchar'', ''binay'', ''varchar'', ''nvarchar'', ''varbinay'')\n\t\t\t\t\t\t\t\tTHEN ''('' + CONVERT(SYSNAME, c.max_length) + '')'' \n\t\t\t\t\t\t\t\tELSE ''''\n\t\t\t\t\t\t\tEND +\n\t\t\t\t\t\t\tCASE WHEN c.is_identity = 1 THEN '' IDENTITY'' ELSE '''' END +\n\t\t\t\t\t\t\tISNULL('' COLLATE '' + c.collation_name, '''') +\n\t\t\t\t\t\t\tCASE WHEN c.is_nullable = 0 THEN '' NOT'' ELSE '''' END + '' NULL'' + CHAR(10)\n\t\t\t\t\t\tFROM sys.columns AS c\n\t\t\t\t\t\t\tINNER JOIN sys.types AS t\n\t\t\t\t\t\t\t\tON t.user_type_id = c.user_type_id\n\t\t\t\t\t\tWHERE c.object_id = tt.type_table_object_id\n\t\t\t\t\t\tORDER BY column_id\n\t\t\t\t\t\tFOR XML PATH('''')), 1, 2, '''')) + '')'' COLLATE DATABASE_DEFAULT AS object_definition\n\tFROM sys.objects AS o\n\t\tINNER JOIN sys.table_types AS tt\n\t\t\tON tt.type_table_object_id = o.object_id\n\tWHERE (tt.type_table_object_id = OBJECT_ID(@object_name) OR @object_name IS NULL)\n' \n\t\n\tINSERT INTO #output (database_name, object_type, object_name, object_definition)\n\tEXECUTE sys.sp_executesql \n        @stmt = @sqlstring\n        , @params = N'@object_name NVARCHAR(257)'\n        , @object_name = @object_name;\n\n\tFETCH NEXT FROM dbs INTO @dbname \nEND \nCLOSE dbs; \nDEALLOCATE dbs; \n\nSELECT @@SERVERNAME AS server_name\n\t\t, database_name\n        , object_type\n\t\t, object_name\n\t\t, object_definition\n        , CONVERT(varchar(100), HASHBYTES('md5', LTRIM(RTRIM(object_definition))),1) AS object_hash \t\t    \n\tFROM #output\n    ORDER BY object_type\n        , object_name\n<\/pre><\/p>\n\n\n\n<p>Executed in my local instance, the result looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_02.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"377\" src=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_02-1024x377.png\" alt=\"\" class=\"wp-image-605\" srcset=\"https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_02-1024x377.png 1024w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_02-300x110.png 300w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_02-768x283.png 768w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_02-150x55.png 150w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_02.png 1290w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p>AS mentioned earlier, each object has got a hash, which later will help to compare them all.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Time to Eat the Elephant<\/h2>\n\n\n\n<p>So far so good, I can use this query to target one or multiple databases at once in the same server, but I do have two instances and still row by row comparison will be a killer, so we need something extra, some sort of super power to go through all of them in on go&#8230; or just use PowerShell!<\/p>\n\n\n\n<p>The approach again is <strong><em>One Object at a Time<\/em><\/strong>, why? because once we have compared one object against all databases, we have the whole picture for that object and we know if it has drifted, where has drifted and the most important, we can start figuring out what to do with it. <\/p>\n\n\n\n<p>I would need:<\/p>\n\n\n\n<ul><li>A master copy with all objects to be the reference <\/li><li>The list of servers I want to target<\/li><li>The list of databases I want to target<\/li><\/ul>\n\n\n\n<p>&nbsp;<\/p>\n\n\n\n<p>And in return, I will get <strong>one folder for each object<\/strong> which will contain<\/p>\n\n\n\n<ul><li>One file with the list of databases where the object is different than the reference<\/li><li>One file per each different copy, named after the hash of the definition of the module<\/li><\/ul>\n\n\n\n<p>&nbsp;<\/p>\n\n\n\n<p>I know it might look a bit rudimentary, but there are multiple free tools for text comparison you can use and since you are doing one object at a time and hopefully there are not that many different versions of the objects, I found it simple enough to do it this way.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Hack<\/h2>\n\n\n\n<p>Time to put all together, the process will:<\/p>\n\n\n\n<ul><li>Get all servers we want to target<\/li><li>Get all object definitions from the master copy<\/li><li>Create the folder structure<\/li><li>Get one object and loop through each server\/database and write down when it&#8217;s different in the right folder.<\/li><\/ul>\n\n\n\n<p><pre class=\"brush: powershell; title: ; notranslate\" title=\"\">\n#Set-executionPolicy -ExecutionPolicy Unrestricted\nImport-Module sqlps;\nClear-Host;\n$StartDate=(Get-Date)\n\n# Config values\n$schema = &quot;AdventureWorksLT&quot;\n$control_srv = 'localhost\\mssql2019';\n$control_db = &quot;AdventureWorksLT&quot;;\n$server_query = &quot;SELECT 'localhost\\MSSQL2019' AS ServerUrl UNION SELECT 'localhost\\MSSQL2019B'&quot;\n\n############################################\n# DO NOT TOUCH BELOW THIS LINE\n############################################\n\n$server_list =  Invoke-Sqlcmd -ServerInstance $control_srv `\n                        -Database $control_db `\n                        -DisableVariables `\n                        -Query $server_query `\n                        -ConnectionTimeout 0 `\n                        -MaxCharLength 64000;\n\n$sql = &quot;SET NOCOUNT ON;\nDECLARE @object_name\tnvarchar(257) = '[@object_name]' \nDECLARE @sqlstring\t\tnvarchar(MAX)\nDECLARE @dbname\t\t\tsysname = '[@dbname]'\n\nSET @dbname = NULLIF(@dbname, '')\nSET @object_name = NULLIF(@object_name, '')\n\nIF OBJECT_ID('tempdb..#output') IS NOT NULL DROP TABLE #output\n\nCREATE TABLE #output  (\n    database_name sysname\t\t\tNOT NULL, \n    object_type nvarchar(60)\t\tNOT NULL, \n    object_name nvarchar(257)\t\tNOT NULL, \n    object_definition nvarchar(MAX) NOT NULL\n)\n\nDECLARE dbs CURSOR FORWARD_ONLY READ_ONLY FAST_FORWARD LOCAL\n\tFOR SELECT name \n            FROM sys.databases \n \t\t    WHERE database_id &amp;gt; 4 \n\t\t        AND state = 0\n\t\t        AND (name = @dbname OR (@dbname IS NULL AND name LIKE 'AdventureWorksLT_%'))\n\nOPEN dbs \nFETCH NEXT FROM dbs INTO @dbname \n\nWHILE @@FETCH_STATUS = 0 BEGIN \n\tSET @sqlstring\t= N'USE ' + QUOTENAME(@dbname) + N'\n\nSET NOCOUNT ON;\n\nSELECT DB_NAME() COLLATE DATABASE_DEFAULT AS [database_name] \n\t\t, o.type_desc COLLATE DATABASE_DEFAULT AS [object_type]\n\t\t, OBJECT_SCHEMA_NAME(o.object_id) + ''.'' + OBJECT_NAME(o.object_id) COLLATE DATABASE_DEFAULT AS [object_name] \n\t\t, LTRIM(RTRIM(OBJECT_DEFINITION(o.OBJECT_ID))) AS [object_definition]\n\tFROM sys.objects AS o\n\tWHERE o.type IN (''TR'',''FN'',''P'',''TF'',''V'')\n        AND (o.object_id = OBJECT_ID(@object_name) OR @object_name IS NULL)\n\nUNION ALL\n\nSELECT DB_NAME() COLLATE DATABASE_DEFAULT AS [database_name]\n\t\t, type_desc COLLATE DATABASE_DEFAULT AS [type_desc] \n\t\t, OBJECT_SCHEMA_NAME(object_id) + ''.'' + name COLLATE DATABASE_DEFAULT AS object_name\n\t\t, ''CREATE TABLE '' + QUOTENAME(OBJECT_SCHEMA_NAME(object_id)) + ''.'' + QUOTENAME(name) + ''('' + CHAR(10) +\n\t\t\t(STUFF(\n\t\t\t\t\t(SELECT '', '' + QUOTENAME(c.name) + '' '' + t.name + \n\t\t\t\t\t\t\tCASE WHEN t.name IN (''char'', ''nchar'', ''binay'', ''varchar'', ''nvarchar'', ''varbinay'')\n\t\t\t\t\t\t\t\tTHEN ''('' + CONVERT(SYSNAME, c.max_length) + '')'' \n\t\t\t\t\t\t\t\tELSE ''''\n\t\t\t\t\t\t\tEND +\n\t\t\t\t\t\t\tCASE WHEN c.is_identity = 1 THEN '' IDENTITY'' ELSE '''' END +\n\t\t\t\t\t\t\tISNULL('' COLLATE '' + c.collation_name, '''') +\n\t\t\t\t\t\t\tCASE WHEN c.is_nullable = 0 THEN '' NOT'' ELSE '''' END + '' NULL'' + CHAR(10)\n\t\t\t\t\t\tFROM sys.columns AS c\n\t\t\t\t\t\t\tINNER JOIN sys.types AS t\n\t\t\t\t\t\t\t\tON t.user_type_id = c.user_type_id\n\t\t\t\t\t\tWHERE c.object_id = o.object_id\n\t\t\t\t\t\tORDER BY column_id\n\t\t\t\t\t\tFOR XML PATH('''')), 1, 2, '''')) + '')'' COLLATE DATABASE_DEFAULT AS object_definition\n\tFROM sys.objects AS o\n\tWHERE type IN (''U'')\n\t\tAND OBJECTPROPERTY(object_id, ''IsMsShipped'') = 0\n\t\tAND (object_id = OBJECT_ID(@object_name) OR @object_name IS NULL)\n\nUNION ALL\n\nSELECT DB_NAME() COLLATE DATABASE_DEFAULT AS database_name \n\t\t, o.type_desc COLLATE DATABASE_DEFAULT\n\t\t, SCHEMA_NAME(tt.schema_id) + ''.'' + tt.name COLLATE DATABASE_DEFAULT AS object_name\n\t\t, ''CREATE TYPE '' + QUOTENAME(SCHEMA_NAME(tt.schema_id)) + ''.'' + QUOTENAME(tt.name) + '' AS TABLE ('' + CHAR(10) +\n\t\t\t(STUFF(\n\t\t\t\t\t(SELECT '', '' + QUOTENAME(c.name) + '' '' + t.name + \n\t\t\t\t\t\t\tCASE WHEN t.name IN (''char'', ''nchar'', ''binay'', ''varchar'', ''nvarchar'', ''varbinay'')\n\t\t\t\t\t\t\t\tTHEN ''('' + CONVERT(SYSNAME, c.max_length) + '')'' \n\t\t\t\t\t\t\t\tELSE ''''\n\t\t\t\t\t\t\tEND +\n\t\t\t\t\t\t\tCASE WHEN c.is_identity = 1 THEN '' IDENTITY'' ELSE '''' END +\n\t\t\t\t\t\t\tISNULL('' COLLATE '' + c.collation_name, '''') +\n\t\t\t\t\t\t\tCASE WHEN c.is_nullable = 0 THEN '' NOT'' ELSE '''' END + '' NULL'' + CHAR(10)\n\t\t\t\t\t\tFROM sys.columns AS c\n\t\t\t\t\t\t\tINNER JOIN sys.types AS t\n\t\t\t\t\t\t\t\tON t.user_type_id = c.user_type_id\n\t\t\t\t\t\tWHERE c.object_id = tt.type_table_object_id\n\t\t\t\t\t\tORDER BY column_id\n\t\t\t\t\t\tFOR XML PATH('''')), 1, 2, '''')) + '')'' COLLATE DATABASE_DEFAULT AS object_definition\n\tFROM sys.objects AS o\n\t\tINNER JOIN sys.table_types AS tt\n\t\t\tON tt.type_table_object_id = o.object_id\n\tWHERE (tt.type_table_object_id = OBJECT_ID(@object_name) OR @object_name IS NULL)\n' \n\t\n\tINSERT INTO #output (database_name, object_type, object_name, object_definition)\n\tEXECUTE sys.sp_executesql \n        @stmt = @sqlstring\n        , @params = N'@object_name NVARCHAR(257)'\n        , @object_name = @object_name;\n\n\tFETCH NEXT FROM dbs INTO @dbname \nEND \nCLOSE dbs; \nDEALLOCATE dbs; \n\nSELECT @@SERVERNAME AS server_name\n\t\t, database_name\n        , object_type\n\t\t, object_name\n\t\t, object_definition\n        , CONVERT(varchar(100), HASHBYTES('md5', LTRIM(RTRIM(object_definition))),1) AS object_hash \t\t    \n\tFROM #output\n    ORDER BY object_type\n        , object_name&quot;;\n\n$master_copy =  Invoke-Sqlcmd -ServerInstance $master_srv `\n                                -Database $master_db `\n                                -DisableVariables `\n                                -Query $sql.Replace(&quot;[@object_name]&quot;, &quot;&quot;).Replace(&quot;[@dbname]&quot;,&quot;$($master_db)&quot;) `\n                                -ConnectionTimeout 0 `\n                                -MaxCharLength 64000;\n\n$object_types = $master_copy | Select-Object object_type -Unique\n\n# to generate a single path per run\n$path = &quot;c:\/temp\/database_schemas\/$($schema)\/$(Get-Date -Format &quot;yyyyMMddHHmmss&quot;)\/&quot;;\nif ( (Test-Path -path $path) -eq $false){\n    #write-output &quot;path does not exist&quot;\n    New-Item -path $path -ItemType Directory | Out-Null;\n}\n\n# to generate a single folder per object_type\nforeach ($type in $object_types){\n    if ( (Test-Path -Path &quot;$($path)\/$($type.object_type)&quot; ) -eq $false){\n        #write-output &quot;path does not exist&quot;\n        New-Item -Path &quot;$($path)\/$($type.object_type)&quot; -ItemType Directory | Out-Null;\n    }    \n}\n\nforeach ($db_object in $master_copy){\n\n    # to cummulate all copies of the object from all servers\n    $local_copy = @();\n    $errors = @();\n\n    Write-Output &quot;Processing $($db_object.object_type) =&amp;amp;amp;amp;amp;gt; $($db_object.object_name)&quot;;\n\n    $db_object_path = &quot;$($path)\/$($db_object.object_type)\/$($db_object.object_name)&quot;;\n        if ( (Test-Path -path $db_object_path) -eq $false){\n            New-Item -path $db_object_path -ItemType Directory | Out-Null;\n    }\n\n    $outputfile = &quot;$($db_object_path)\/all_diffs.txt&quot;;\n    $db_object | Select-Object database_name, object_name, object_hash | Out-File $outputfile -Append;\n\n    foreach ($srv in $server_list){\n    \n        Write-Output &quot;`tProcessing server $($srv.ServerUrl)&quot;;\n\n        $objDataTable = Invoke-Sqlcmd -ServerInstance &quot;$($srv.ServerUrl)&quot; `\n                                -Database &quot;master&quot; `\n                                -DisableVariables `\n                                -Query $sql.Replace(&quot;[@object_name]&quot;, $db_object.object_name).Replace(&quot;[@dbname]&quot;,&quot;&quot;) `\n                                -ConnectionTimeout 0 `\n                                -MaxCharLength 64000;                                \n\n        if ($null -ne $objDataTable){\n            $local_copy += $objDataTable;\n        }\n        else{\n            $errors += [pscustomobject]@{server_name=$db.ServerUrl;database_name=$db.database_name;object_name=$db_object.object_name;object_hash=0x0};\n        }\n    }\n    \n    # Drifts are any row that does not match the object_hash\n    # $local_copy | Select-Object database_name, objecT_hash | Where-Object -Property object_hash -ne $db_object.object_hash\n    $drifts = $local_copy | Select-Object | Where-Object -Property object_hash -ne $db_object.object_hash\n\n    if ($drifts.Count -eq 0){\n    #if no drifts for this object, remove the noise\n        Remove-Item -Path $db_object_path -Recurse;\n    }\n    else{\n        # if there was any drift, get unique versions from all servers\n        $diff_versions = $drifts | Select-Object object_hash, object_definition -Unique;\n\n        # add each database that drifted to the list\n        $errors | Select-Object server_name, database_name, object_name, object_hash | Out-File $outputfile -Append;\n        $drifts | Select-Object server_name, database_name, object_name, object_hash | Sort-Object -Property object_hash | Out-File $outputfile -Append;\n        \n        # put the definition from the master object \n        $db_object.object_definition | Out-File &quot;$($db_object_path)\/$($db_object.object_name).sql&quot; \n\n        foreach ($diff in $($diff_versions)){\n            $diff.object_definition | Out-File &quot;$($db_object_path)\/$($diff.object_hash).sql&quot; \n        }\n    }\n}\n\n$EndDate=(Get-Date);\n\nNew-TimeSpan \u2013Start $StartDate \u2013End $EndDate | Format-Table -AutoSize;\n<\/pre><\/p>\n\n\n\n<p>Once the script completes a run, you can go to your local dir c:\\temp <\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_03-1.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img loading=\"lazy\" decoding=\"async\" width=\"949\" height=\"498\" src=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_03-1.png\" alt=\"\" class=\"wp-image-607\" srcset=\"https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_03-1.png 949w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_03-1-300x157.png 300w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_03-1-768x403.png 768w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_03-1-150x79.png 150w\" sizes=\"(max-width: 949px) 100vw, 949px\" \/><\/a><\/figure>\n\n\n\n<p>There is one folder per [object_type] that contains one folder for each object that has drifted. If the object is the same in all databases, the process will take care of it and remove the noise, so you can focus on what is important.<\/p>\n\n\n\n<p>On each folder that represents an object, you can see different files:<\/p>\n\n\n\n<ul><li>The master copy of the object, named after it.<\/li><li>all_diffs.txt, which will contain a list of databases where the object has drifted. <br>This is the most important file, because it identifies which database has which version of the object.<\/li><li>One file per different version named after their hashes, you should be fine to compare this in a text editor.<\/li><\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_05.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img loading=\"lazy\" decoding=\"async\" width=\"930\" height=\"432\" src=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_05.png\" alt=\"\" class=\"wp-image-608\" srcset=\"https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_05.png 930w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_05-300x139.png 300w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_05-768x357.png 768w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_05-150x70.png 150w\" sizes=\"(max-width: 930px) 100vw, 930px\" \/><\/a><\/figure>\n\n\n\n<p>Using the free tool WinMerge, you can compare three files at a time, which is quite handy.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"286\" src=\"https:\/\/sqldoubleg.live-website.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06-1024x286.png\" alt=\"\" class=\"wp-image-609\" srcset=\"https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06-1024x286.png 1024w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06-300x84.png 300w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06-768x214.png 768w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06-1536x428.png 1536w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06-150x42.png 150w, https:\/\/www.sqldoubleg.com\/wp-content\/uploads\/2021\/07\/how_to_compare_1000_dbs_06.png 1721w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Keeping databases schemas in sync is a big thing for me now, so trust me when I say doing this was bugging me for a while until I decided (and got the time) to get it done. <\/p>\n\n\n\n<p>I developed this script\/tool based on my requirements and yours might be different, but I am pretty sure you still can get some good ideas and draw a plan that fits your needs.<\/p>\n\n\n\n<p>I really enjoy this kind of challenges and it is amazing how much we can do with a little bit of TSQL and a little bit of PowerShell.<\/p>\n\n\n\n<p>You can find the scripts in <a href=\"https:\/\/github.com\/SQLDoubleG\/get-schema-drifts\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub<\/a>, I hope you find it useful and please let me know in the comments what you think.<\/p>\n\n\n\n<p>Thanks for reading.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Compare database schemas can be tricky, here I show you how I managed to compare thousands of databases&#8217; schemas at once&nbsp; Working with databases is always fun and you never know what&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,19],"tags":[89,15],"_links":{"self":[{"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/posts\/602"}],"collection":[{"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/comments?post=602"}],"version-history":[{"count":0,"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/posts\/602\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/media?parent=602"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/categories?post=602"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sqldoubleg.com\/es\/wp-json\/wp\/v2\/tags?post=602"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}