Sometimes you want to be able to dynamically construct table definitions to perform different actions, such as creating a copy of a table on a different database, compare table schema, sync table data where the tables are not guarenteed to have the same schema, generate update, merge, select and insert statements and not have to keep adjusting the code as the table schema drifts.
Below we will go through a few of these scenarios, but first lets define the “black magic” that the rest of the examples will build upon.
Black Magic
These are the exact same query but produce a #Source and #Destination temp tables that the examples will use.
Select Source Schema
DECLARE @TableName SYSNAME = 'MyTable', @TableSchema SYSNAME = 'dbo'
IF OBJECT_ID('tempdb..#Source') IS NOT NULL DROP TABLE #Source
SELECT [ColumnId] = sc.[column_id], [ColumnName] = sc.[name] COLLATE DATABASE_DEFAULT, -- adjust timestamp type to varbinary [Type] = CASE WHEN st.[user_type_id] = 189 THEN 'varbinary' ELSE st.[name] END COLLATE DATABASE_DEFAULT, -- adjust nvarchar length to half what is reported since it takes 2 bytes to store unicode, and adjust timestamp to the translated varbinary length [Length] = CASE WHEN sc.[max_length] = -1 THEN -1 WHEN st.[user_type_id] = 231 THEN sc.[max_length] / 2 WHEN st.[user_type_id] = 189 THEN 85 ELSE sc.[max_length] END, [Precision] = sc.[precision], [Scale] = sc.[scale], [IsNullable] = sc.[is_nullable]INTO #SourceFROM [sys].[objects] soJOIN [sys].[columns] scON so.[object_id] = sc.[object_id]JOIN [sys].[types] stON sc.[user_type_id] = st.[user_type_id]JOIN [sys].[schemas] ssON so.[schema_id] = ss.[schema_id]WHERE so.[name] = @TableName AND ss.[name] = @TableSchemaSelect Destination Schema
DECLARE @TableName SYSNAME = 'MyTable', @TableSchema SYSNAME = 'dbo'
IF OBJECT_ID('tempdb..#Destination') IS NOT NULL DROP TABLE #Destination
SELECT [ColumnId] = sc.[column_id], [ColumnName] = sc.[name] COLLATE DATABASE_DEFAULT, -- adjust timestamp type to varbinary [Type] = CASE WHEN st.[user_type_id] = 189 THEN 'varbinary' ELSE st.[name] END COLLATE DATABASE_DEFAULT, -- adjust nvarchar length to half what is reported since it takes 2 bytes to store unicode, and adjust timestamp to the translated varbinary length [Length] = CASE WHEN sc.[max_length] = -1 THEN -1 WHEN st.[user_type_id] = 231 THEN sc.[max_length] / 2 WHEN st.[user_type_id] = 189 THEN 85 ELSE sc.[max_length] END, [Precision] = sc.[precision], [Scale] = sc.[scale], [IsNullable] = sc.[is_nullable]INTO #DestinationFROM [sys].[objects] soJOIN [sys].[columns] scON so.[object_id] = sc.[object_id]JOIN [sys].[types] stON sc.[user_type_id] = st.[user_type_id]JOIN [sys].[schemas] ssON so.[schema_id] = ss.[schema_id]WHERE so.[name] = @TableName AND ss.[name] = @TableSchemaExample — Create Table Copy
Create a copy of the table using the source schema information possibly injecting extra columns in the copy.
DECLARE @TableName SYSNAME = 'MyTable', @TableSchema SYSNAME = 'dbo', @IsDebug BIT = 0
DECLARE @DataType NVARCHAR(200), @Length INT, @Precision INT, @Scale INT, @IsNullable INT, @ColumnName NVARCHAR(200), @Statement NVARCHAR(MAX) = ''
DECLARE x CURSOR FAST_FORWARD FOR SELECT [ColumnName], [Type], [Length], [Precision], [Scale], [IsNullable] FROM #Source ORDER BY [ColumnId]
OPEN xFETCH NEXT FROM x INTO @ColumnName, @DataType, @Length, @Precision, @Scale, @IsNullableWHILE @@FETCH_STATUS = 0BEGIN DECLARE @NullClause NVARCHAR(10) = ' NULL' IF @IsNullable = 0 BEGIN SELECT @NullClause = ' NOT NULL' END
IF @DataType = 'timestamp' BEGIN SELECT @DataType = 'varbinary' SELECT @Length = 85 END
IF @DataType IN('char','varchar','binary','varbinary','nvarchar','nchar') BEGIN SELECT @Statement = @Statement + '[' + @ColumnName + '] ' + @DataType + '(' + ISNULL(NULLIF(CONVERT(NVARCHAR,@Length), '-1'), 'MAX') + ')' + @NullClause + ',' END ELSE IF @DataType IN('decimal','numeric') BEGIN SELECT @Statement = @Statement + '[' + @ColumnName + '] ' + @DataType + '(' + CONVERT(NVARCHAR,@Precision) + ',' + CONVERT(NVARCHAR,@Scale) + ')' + @NullClause + ',' END ELSE BEGIN SELECT @Statement = @Statement + '[' + @ColumnName + '] ' + @DataType + @NullClause + ',' ENDFETCH NEXT FROM x INTO @ColumnName, @DataType, @Length, @Precision, @Scale, @IsNullableENDCLOSE xDEALLOCATE x
SELECT @Statement = SUBSTRING(@Statement, 1, LEN(@Statement) - 1)
DECLARE @Sql NVARCHAR(MAX) = ''
SELECT @Sql = 'CREATE TABLE [{0}].[{1}]({2})'
SELECT @Sql = REPLACE(@Sql, '{0}', @TableSchema)SELECT @Sql = REPLACE(@Sql, '{1}', @TableName)SELECT @Sql = REPLACE(@Sql, '{2}', @Statement)
IF @IsDebug = 1 PRINT (@Sql)IF @IsDebug = 0 EXEC (@Sql)Example — Schema Diff
Below are a few ways you can use this information to find;
- columns that have changed
- columns that are missing from the destination
- columns that have been removed from the source and not the destination
- columns that both tables have in common
IF OBJECT_ID('tempdb..#CHANGED_COLUMNS') IS NOT NULL DROP TABLE #CHANGED_COLUMNS
SELECT [ColumnName], [Type], [Length], [Precision], [Scale]INTO #CHANGED_COLUMNSFROM #SourceEXCEPTSELECT [ColumnName], [Type], [Length], [Precision], [Scale]FROM #Destination
IF OBJECT_ID('tempdb..#MISSING_COLUMNS') IS NOT NULL DROP TABLE #MISSING_COLUMNS
SELECT [ColumnName]INTO #MISSING_COLUMNSFROM #SourceEXCEPTSELECT [ColumnName]FROM #Destination
IF OBJECT_ID('tempdb..#DELETED_COLUMNS') IS NOT NULL DROP TABLE #DELETED_COLUMNS
SELECT [ColumnName]INTO #DELETED_COLUMNSFROM #DestinationEXCEPTSELECT [ColumnName]FROM #SOURCE
IF OBJECT_ID('tempdb..#SHARED_COLUMNS') IS NOT NULL DROP TABLE #SHARED_COLUMNS
SELECT [ColumnName]INTO #SHARED_COLUMNSFROM #SourceINTERSECTSELECT [ColumnName]FROM #DestinationExample — Sync Destination Schema with Source
Here we will cursor over the columns that are missing and either create a series of ALTER TABLE statements to bring the destination table in sync with the source.
DECLARE @TableName SYSNAME = 'MyTable', @TableSchema SYSNAME = 'dbo', @IsDebug BIT = 0
IF OBJECT_ID('tempdb..#CHANGED_COLUMNS') IS NOT NULL DROP TABLE #CHANGED_COLUMNS
SELECT [ColumnName], [Type], [Length], [Precision], [Scale]INTO #CHANGED_COLUMNSFROM #SourceEXCEPTSELECT [ColumnName], [Type], [Length], [Precision], [Scale]FROM #Destination
DECLARE @Statement NVARCHAR(MAX) = '', @AlterColumnStatement NVARCHAR(MAX) = '', @ColumnName NVARCHAR(200), @Sql NVARCHAR(MAX) = ''
IF EXISTS (SELECT 1 FROM #CHANGED_COLUMNS)BEGIN DECLARE @DataType NVARCHAR(200), @Length INT, @Precision INT, @Scale INT
DECLARE @HasExistingColumns INT = 0, @HasNewColumns INT = 0
DECLARE x CURSOR FOR SELECT [ColumnName], [Type], [Length], [Precision], [Scale] FROM #COLUMNS ORDER BY 1
OPEN x FETCH NEXT FROM x INTO @ColumnName, @DataType, @Length, @Precision, @Scale WHILE @@FETCH_STATUS = 0 BEGIN DECLARE @Part NVARCHAR(MAX) DECLARE @AlterColumnStub NVARCHAR(MAX) = 'ALTER TABLE [{2}].[{0}]ALTER COLUMN {1}'
DECLARE @ColumnExists INT = 0 IF EXISTS(SELECT 1 FROM #Destination WHERE [ColumnName] = @ColumnName) BEGIN SELECT @ColumnExists = 1 SELECT @HasExistingColumns = 1 END ELSE BEGIN SELECT @HasNewColumns = 1 END
IF @DataType = 'timestamp' BEGIN SELECT @DataType = 'varbinary' SELECT @Length = 85 END
IF @DataType IN('char','varchar','binary','varbinary','nvarchar','nchar') BEGIN SELECT @Part = '[' + @ColumnName + '] ' + @DataType + '(' + ISNULL(NULLIF(CONVERT(NVARCHAR,@Length), '-1'), 'MAX') + ')'
IF(@ColumnExists = 1) BEGIN SELECT @AlterColumnStatement = @AlterColumnStatement + REPLACE(@AlterColumnStub, '{1}', @Part) END ELSE BEGIN SELECT @Statement = @Statement + @Part + ',' END END ELSE IF @DataType IN('decimal','numeric') BEGIN SELECT @Part = '[' + @ColumnName + '] ' + @DataType + '(' + CONVERT(NVARCHAR,@Precision) + ',' + CONVERT(NVARCHAR,@Scale) + ')'
IF(@ColumnExists = 1) BEGIN SELECT @AlterColumnStatement = @AlterColumnStatement + REPLACE(@AlterColumnStub, '{1}', @Part) END ELSE BEGIN SELECT @Statement = @Statement + @Part + ',' END END ELSE BEGIN SELECT @Part = '[' + @ColumnName + '] ' + @DataType
IF(@ColumnExists = 1) BEGIN SELECT @AlterColumnStatement = @AlterColumnStatement + REPLACE(@AlterColumnStub, '{1}', @Part) END ELSE BEGIN SELECT @Statement = @Statement + @Part + ',' END END FETCH NEXT FROM x INTO @ColumnName, @DataType, @Length, @Precision, @Scale END CLOSE x DEALLOCATE x
IF (LEN(@Statement) > 1) BEGIN SELECT @Statement = SUBSTRING(@Statement, 1, LEN(@Statement) - 1) END
-- determine if is alter or create statement IF OBJECT_ID(@EntityName) IS NOT NULL BEGIN IF (@HasExistingColumns = 1) BEGIN SELECT @Sql += @AlterColumnStatement END
IF (@HasNewColumns = 1) BEGIN SELECT @Sql += 'ALTER TABLE [{2}].[{0}]ADD {1}' END END
SELECT @Sql = REPLACE(@Sql, '{0}', @TableName) SELECT @Sql = REPLACE(@Sql, '{1}', @Statement) SELECT @Sql = REPLACE(@Sql, '{2}', @TableSchema)
IF @IsDebug = 1 PRINT (@Sql) IF @IsDebug = 0 EXEC (@Sql)ENDExample — Generate Statement Layouts
Here we generate layouts that can be used in SELECT, INSERT and UPDATE statements assuming the source table is aliased as [source].
IF OBJECT_ID('tempdb..#SHARED_COLUMNS') IS NOT NULL DROP TABLE #SHARED_COLUMNS
SELECT [ColumnName]INTO #SHARED_COLUMNSFROM #SourceINTERSECTSELECT [ColumnName]FROM #Destination
-- generate layout --DECLARE @ColumnFormat NVARCHAR(MAX) = '', @UpdateFormat NVARCHAR(MAX) = '', @ValuesFormat NVARCHAR(MAX) = ''
SELECT @ColumnFormat = STUFF((SELECT ', [' + [ColumnName] + ']' FROM #SHARED_COLUMNS ORDER BY [ColumnName] FOR XML PATH('')), 1, 2, ''), @UpdateFormat = STUFF((SELECT ', [' + [ColumnName] + '] = [source].[' + [ColumnName] + ']' FROM #SHARED_COLUMNS ORDER BY [ColumnName] FOR XML PATH('')), 1, 2, ''), @ValuesFormat = STUFF((SELECT ', [source].[' + [ColumnName] + ']' FROM #SHARED_COLUMNS ORDER BY [ColumnName] FOR XML PATH('')), 1, 2, '')
PRINT @ColumnFormatPRINT @UpdateFormatPRINT @ValuesFormat~ SK