Let SQL Server Write Code for You
top of page

Let SQL Server Write Code for You

Introduction

Many times I happen to find myself in a situation where, as a DBA, I need to write some long and cumbersome code (usually for maintenance purposes).

For example:

  1. Write a script to copy a bunch of tables from one database to another.

  2. Import the tables from one database to another database.

  3. Rebuild a bunch of indexes.

  4. Update a bunch of statistics.

  5. Write scripts for querying a lot of tables.

  6. Write scripts to backup or restore a lot of databases.

  7. And so on and so forth.

A lot of DBAs would spend hours (and even days) sitting in front of their computer and write a stupendous amount of lines of code… Then take care of a lot of copy-paste and typing errors… Test the code and find errors… Fix the code… Run it again… And so on. All the while having to navigate within a huge forest of code.

I wouldn’t think that it takes a special kind of mind to stand up and say “just hold on a minute! Why am I working so hard with this mundane task?? Why can’t SQL Server do all of this for me??” Because in fact… It can!

If you were working with SQL Server for more than a couple months, most probably you’ve heard about ‘Dynamic SQL’, right? I personally covered several topics about it in this blog of mine (namely, SQL Injection among other things).

Also, I hope you heard about the huge list of ‘system tables’ and ‘catalog views’ that SQL Server has to offer, right? And finally, you should have also heard that you can concatenate a list of values using queries, right?

If you haven’t yet, then now is your chance. Because we’re about to bring all of these elements together to make SQL Server do your mundane work for you! [cue maniacal laughter now]

Simple Concept Example: Auto-Generate Insertion Procedures

Here’s a basic scenario to demonstrate the concept for what I’m talking about.

The task goes like this:

You have a list of tables, and you need to generate a simple insertion procedure per each of the tables. Each stored procedure should receive as parameters the values for each of the table columns, except the IDENTITY column.

Alright, so first, let’s take one such table as an example and see how such a procedure should look like. This is the table we’ll use for our example:

Transact-SQL

CREATE TABLE [dbo].[Invoices]( [InvoiceID] int IDENTITY(1,1) PRIMARY KEY, [Title] nvarchar(50), [OrderDate] date, [Qty] int, [Total] money )

1

2

3

4

5

6

7

CREATE TABLE [dbo].[Invoices](

[InvoiceID] int IDENTITY(1,1) PRIMARY KEY,

[Title] nvarchar(50),

[OrderDate] date,

[Qty] int,

[Total] money

)

An insertion procedure for such a table would look like this:

Transact-SQL

CREATE PROCEDURE [InvoicesInsert] @Title nvarchar(50), @OrderDate date, @Qty int, @Total money AS INSERT INTO [dbo].[Invoices] ([Title], [OrderDate], [Qty], [Total]) VALUES(@Title, @OrderDate, @Qty, @Total)

1

2

3

4

5

6

7

8

9

CREATE PROCEDURE [InvoicesInsert]

@Title nvarchar(50),

@OrderDate date,

@Qty int,

@Total money

AS

INSERT INTO [dbo].[Invoices] ([Title], [OrderDate], [Qty], [Total])

VALUES(@Title, @OrderDate, @Qty, @Total)

All right, now let’s try and break it down and isolate the per-table dynamic parts:

Transact-SQL

CREATE PROCEDURE [{TableName}Insert] {ColumnParametersListWithTypes} AS INSERT INTO [{TableName}] ({ColumnsList}) VALUES({ColumnParametersList})

1

2

3

4

5

6

CREATE PROCEDURE [{TableName}Insert]

{ColumnParametersListWithTypes}

AS

INSERT INTO [{TableName}] ({ColumnsList})

VALUES({ColumnParametersList})

Yeah, this looks much simpler. We’ll call this our ‘template’. Now let’s review each dynamic part and see how we can generate them:

  1. {TableName} This one should be a no-brainer, seeing as we’ll be receiving this as a pre-set value.

  2. {ColumnsList} This one should be fairly simple to get. We can use the sys.columns catalog view to get the list of columns per each table. We’ll just need to concatenate them all with commas between them and we’re set. Also, the is_identity column in the sys.columns catalog view will help us determine which of the columns has an IDENTITY property.

  3. {ColumnParametersList} This one is very similar to {ColumnsList}, except we just need to add the @ symbol before each column.

  4. {ColumnParametersListWithTypes} This one is a little trickier, because it needs to include the relevant data type of each column, and not just the name. No fear, though! The sys.types catalog view, and some CASE WHEN scripting will help us here.

Right, so let’s go right in and see how we can implement this:

Transact-SQL

DECLARE @ColumnsList NVARCHAR(MAX); DECLARE @ColumnParametersList NVARCHAR(MAX); DECLARE @ColumnParametersListWithTypes NVARCHAR(MAX); SELECT @ColumnsList = ISNULL(@ColumnsList + N', ', N'') + QUOTENAME(c.name), @ColumnParametersList = ISNULL(@ColumnParametersList + N', ', N'') + '@' + REPLACE(c.name, ' ', ''), @ColumnParametersListWithTypes = ISNULL(@ColumnParametersListWithTypes + N', ', N'') + '@' + REPLACE(c.name, ' ', '') + N' ' + CASE WHEN t.name IN ('char','varchar','nchar','nvarchar','varbinary','binary') THEN t.name + '('+ CASE WHEN c.max_length=-1 THEN 'MAX' ELSE CONVERT(VARCHAR(4), CASE WHEN t.name IN ('nchar','nvarchar') THEN c.max_length/2 ELSE c.max_length END ) END + ')' WHEN t.name IN ('decimal','numeric') THEN t.name + '(' + CONVERT(VARCHAR(4),c.precision) + ',' + CONVERT(VARCHAR(4),c.Scale) + ')' ELSE t.name END FROM sys.columns AS c INNER JOIN sys.types AS t ON c.user_type_id = t.user_type_id where c.object_id = object_id(@TableName) AND c.is_identity = 0 ORDER BY c.column_id PRINT @ColumnsList PRINT @ColumnParametersList PRINT @ColumnParametersListWithTypes

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

DECLARE @ColumnsList NVARCHAR(MAX);

DECLARE @ColumnParametersList NVARCHAR(MAX);

DECLARE @ColumnParametersListWithTypes NVARCHAR(MAX);

SELECT

@ColumnsList = ISNULL(@ColumnsList + N', ', N'') + QUOTENAME(c.name),

@ColumnParametersList = ISNULL(@ColumnParametersList + N', ', N'') + '@' + REPLACE(c.name, ' ', ''),

@ColumnParametersListWithTypes = ISNULL(@ColumnParametersListWithTypes + N', ', N'') + '@' + REPLACE(c.name, ' ', '')

+ N' ' + CASE WHEN t.name IN

('char','varchar','nchar','nvarchar','varbinary','binary')

THEN t.name + '('+

CASE WHEN c.max_length=-1 THEN 'MAX'

ELSE CONVERT(VARCHAR(4),

CASE WHEN t.name IN ('nchar','nvarchar')

THEN c.max_length/2 ELSE c.max_length END )

END + ')'

WHEN t.name IN ('decimal','numeric')

THEN t.name + '('

+ CONVERT(VARCHAR(4),c.precision) + ','

+ CONVERT(VARCHAR(4),c.Scale)

+ ')'

ELSE t.name END

FROM sys.columns AS c

INNER JOIN sys.types AS t

ON c.user_type_id = t.user_type_id

where

c.object_id = object_id(@TableName)

AND c.is_identity = 0

ORDER BY c.column_id

PRINT @ColumnsList

PRINT @ColumnParametersList

PRINT @ColumnParametersListWithTypes

This script uses concatenation and queries from a couple catalog views, to create 3 lists of values.

If we put ‘Invoices’ in our @TableName variable, we’ll get the following output:

Transact-SQL

[Title], [OrderDate], [Qty], [Total] @Title, @OrderDate, @Qty, @Total @Title nvarchar(50), @OrderDate date, @Qty int, @Total money

1

2

3

[Title], [OrderDate], [Qty], [Total]

@Title, @OrderDate, @Qty, @Total

@Title nvarchar(50), @OrderDate date, @Qty int, @Total money

Pretty neat, huh?

Right, so now let’s bring it all together by building the relevant TSQL commands:

Transact-SQL

DECLARE @DropCommand NVARCHAR(MAX) DECLARE @CreateCommand NVARCHAR(MAX) SET @DropCommand = N'IF OBJECT_ID(''' + QUOTENAME(@TableName + 'Insert') + N''') IS NOT NULL DROP PROCEDURE ' + QUOTENAME(@TableName + 'Insert') SET @CreateCommand = N'CREATE PROCEDURE ' + QUOTENAME(@TableName + 'Insert') + N' ' + @ColumnParametersListWithTypes + N' AS INSERT INTO ' + QUOTENAME(@TableName) + N' (' + @ColumnsList + N') VALUES (' + @ColumnParametersList + N')' PRINT @DropCommand PRINT 'GO' PRINT @CreateCommand PRINT 'GO'

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

DECLARE @DropCommand NVARCHAR(MAX)

DECLARE @CreateCommand NVARCHAR(MAX)

SET @DropCommand =

N'IF OBJECT_ID(''' + QUOTENAME(@TableName + 'Insert') + N''') IS NOT NULL

DROP PROCEDURE ' + QUOTENAME(@TableName + 'Insert')

SET @CreateCommand =

N'CREATE PROCEDURE ' + QUOTENAME(@TableName + 'Insert') + N'

' + @ColumnParametersListWithTypes + N'

AS

INSERT INTO ' + QUOTENAME(@TableName) + N' (' + @ColumnsList + N')

VALUES (' + @ColumnParametersList + N')'

PRINT @DropCommand

PRINT 'GO'

PRINT @CreateCommand

PRINT 'GO'

And the output for our table would be:

Transact-SQL

IF OBJECT_ID('[InvoicesInsert]') IS NOT NULL DROP PROCEDURE [InvoicesInsert] GO CREATE PROCEDURE [InvoicesInsert] @Title nvarchar(50), @OrderDate date, @Qty int, @Total money AS INSERT INTO [Invoices] ([Title], [OrderDate], [Qty], [Total]) VALUES (@Title, @OrderDate, @Qty, @Total) GO

1

2

3

4

5

6

7

8

9

IF OBJECT_ID('[InvoicesInsert]') IS NOT NULL

DROP PROCEDURE [InvoicesInsert]

GO

CREATE PROCEDURE [InvoicesInsert]

@Title nvarchar(50), @OrderDate date, @Qty int, @Total money

AS

INSERT INTO [Invoices] ([Title], [OrderDate], [Qty], [Total])

VALUES (@Title, @OrderDate, @Qty, @Total)

GO

Success! So now we just need to implement this logic in a cursor which traverses several tables, and we’re done. You can download the full sample script from here:


Note that you can use similar methods to generate UPDATE and DELETE procedures as well, just to name a few. The catalog views sys.indexes and sys.index_columns will be helpful for determining the primary key column(s) per each table.

Real-Life Case Study: Copy a List of Tables via Linked Server

So now let’s dive right in, and review a more complex example of a real-life scenario that I’ve recently encountered:

Company X has requested to build a script that would incrementally copy a list of tables from one SQL Server to another using a Linked Server connection. The incremental copy should be done using a numerical incremental primary column which exists in every table. Also, if a table doesn’t exist yet in the destination database, it should be automatically created.

There are several key elements of SQL Server which we’re going to leverage for our benefit here:

  1. Dynamic SQL

  2. System Catalog Views

  3. Concatenation

  4. Values Constructor

Here’s what I did:

First, I initialized the following cursor:

Transact-SQL

DECLARE @Table SYSNAME, @PKColumn SYSNAME, @PKColumnType SYSNAME; DECLARE Cur CURSOR FOR SELECT TableName , PKColumn = QUOTENAME(c.name) , PKColumnType = t.name + CASE WHEN t.name IN ('decimal','numeric') THEN '('+ CONVERT(VARCHAR(4),c.precision)+',' + CONVERT(VARCHAR(4),c.Scale)+')' ELSE '' END FROM (VALUES ('Table1'), ('Table2'), ('Table3'), ('Table4'), ('Table5') ) AS A(TableName) INNER JOIN sys.indexes AS i ON i.object_id = OBJECT_ID(TableName) INNER JOIN sys.index_columns AS ic ON i.OBJECT_ID = ic.OBJECT_ID AND i.index_id = ic.index_id INNER JOIN sys.columns AS c ON i.object_id = c.object_id AND ic.column_id = c.column_id INNER JOIN sys.types AS t ON t.system_type_id = c.system_type_id WHERE i.is_primary_key = 1 ORDER BY TableName

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

DECLARE @Table SYSNAME, @PKColumn SYSNAME, @PKColumnType SYSNAME;

DECLARE Cur CURSOR FOR

SELECT

TableName

, PKColumn = QUOTENAME(c.name)

, PKColumnType = t.name

+ CASE WHEN t.name IN ('decimal','numeric')

THEN '('+ CONVERT(VARCHAR(4),c.precision)+','

+ CONVERT(VARCHAR(4),c.Scale)+')'

ELSE '' END

FROM

(VALUES

('Table1'),

('Table2'),

('Table3'),

('Table4'),

('Table5')

) AS A(TableName)

INNER JOIN

sys.indexes AS i

ON

i.object_id = OBJECT_ID(TableName)

INNER JOIN

sys.index_columns AS ic

ON

i.OBJECT_ID = ic.OBJECT_ID

AND i.index_id = ic.index_id

INNER JOIN

sys.columns AS c

ON

i.object_id = c.object_id

AND ic.column_id = c.column_id

INNER JOIN

sys.types AS t

ON

t.system_type_id = c.system_type_id

WHERE

i.is_primary_key = 1

ORDER BY TableName

Note how this cursor makes use of the ‘Values Constructor’ by specifically preparing a list of values to be used as table names, and then joins it with various catalog views in order to find the name of the primary key column per each table. Also note that I don’t necessarily know the data type of each primary column in every table, it can be either Int or Bigint or Decimal or Numeric. If a column is Decimal/Numeric, its scale and precision would also need to be known (that’s the reason for the CASE statement above).

Here’s a sample result of such a cursor from my sandbox database:TableNamePKColumnPKColumnTypeFileStorage[ID]intInvoiceHeader[InvoiceId]intInvoiceRows[RowID]intInvoices[InvoiceID]intSimpleFileStorage[ID]int

Now that I have myself a list of table names, the names of the primary key column per each and even its type, I’m ready to start building the commands themselves.

First, I found some ready-made dynamic table creation script on the internet, cleaned it up a bit, and adjusted it for my own script (it’s the same one I used for generating the column types in the previous example):

Transact-SQL

OPEN Cur FETCH NEXT FROM Cur INTO @Table, @DoChunks, @PKColumn, @PKColumnType WHILE @@FETCH_STATUS = 0 BEGIN -- if table not exists, create it DECLARE @CMD NVARCHAR(MAX), @ColumnInsertionList NVARCHAR(MAX) SET @CMD = NULL; SET @ColumnInsertionList = NULL; SELECT @CMD = ISNULL(@CMD + N', ', N'') + QUOTENAME(c.name) + N' ' + CASE WHEN t.name IN ('char', 'varchar','nchar','nvarchar') THEN t.name + '('+ CASE WHEN c.max_length=-1 THEN 'MAX' ELSE CONVERT(VARCHAR(4), CASE WHEN t.name IN ('nchar','nvarchar') THEN c.max_length/2 ELSE c.max_length END ) END +')' WHEN t.name IN ('decimal','numeric') THEN t.name + '('+ CONVERT(VARCHAR(4),c.precision)+',' + CONVERT(VARCHAR(4),c.Scale)+')' ELSE t.name END + CASE WHEN c.is_nullable=0 THEN ' NOT NULL' ELSE ' NULL' END + CASE WHEN c.default_object_id <>0 THEN ' DEFAULT '+object_Definition(c.default_object_id) ELSE '' END + CASE WHEN c.collation_name IS NULL THEN '' WHEN c.collation_name<> (SELECT collation_name FROM sys.databases WHERE name=DB_NAME()) COLLATE Latin1_General_CI_AS THEN COALESCE(' COLLATE '+c.collation_name,'') ELSE '' END , @ColumnInsertionList = ISNULL(@ColumnInsertionList + N', ', N'') + QUOTENAME(c.name) FROM sys.columns AS c INNER JOIN sys.types AS t ON t.system_type_id = c.system_type_id WHERE object_id = OBJECT_ID(@Table) ORDER BY column_id ASC SET @CMD = N'USE MyTargetDB; IF OBJECT_ID(''' + @Table + N''') IS NULL CREATE TABLE ' + QUOTENAME(@Table) + N'( ' + @CMD + N' );' EXECUTE (@CMD) AT [MyTargetLinkedServer]

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

OPEN Cur

FETCH NEXT FROM Cur INTO @Table, @DoChunks, @PKColumn, @PKColumnType

WHILE @@FETCH_STATUS = 0

BEGIN

-- if table not exists, create it

DECLARE @CMD NVARCHAR(MAX), @ColumnInsertionList NVARCHAR(MAX)

SET @CMD = NULL;

SET @ColumnInsertionList = NULL;

SELECT @CMD = ISNULL(@CMD + N',

', N'') + QUOTENAME(c.name)

+ N' '

+ CASE

WHEN t.name IN ('char', 'varchar','nchar','nvarchar') THEN t.name + '('+

CASE WHEN c.max_length=-1 THEN 'MAX'

ELSE CONVERT(VARCHAR(4),

CASE WHEN t.name IN ('nchar','nvarchar')

THEN c.max_length/2 ELSE c.max_length END )

END +')'

WHEN t.name IN ('decimal','numeric')

THEN t.name + '('+ CONVERT(VARCHAR(4),c.precision)+','

+ CONVERT(VARCHAR(4),c.Scale)+')'

ELSE t.name END

+ CASE WHEN c.is_nullable=0 THEN ' NOT NULL' ELSE ' NULL' END

+ CASE WHEN c.default_object_id <>0

THEN ' DEFAULT '+object_Definition(c.default_object_id) ELSE '' END

+ CASE WHEN c.collation_name IS NULL THEN ''

WHEN c.collation_name<>

(SELECT collation_name FROM sys.databases

WHERE name=DB_NAME()) COLLATE Latin1_General_CI_AS

THEN COALESCE(' COLLATE '+c.collation_name,'') ELSE '' END

, @ColumnInsertionList = ISNULL(@ColumnInsertionList + N', ', N'') + QUOTENAME(c.name)

FROM

sys.columns AS c

INNER JOIN

sys.types AS t

ON

t.system_type_id = c.system_type_id

WHERE object_id = OBJECT_ID(@Table)

ORDER BY column_id ASC

SET @CMD = N'USE MyTargetDB;

IF OBJECT_ID(''' + @Table + N''') IS NULL

CREATE TABLE ' + QUOTENAME(@Table) + N'(

' + @CMD + N'

);'

EXECUTE (@CMD) AT [MyTargetLinkedServer]

Note several important things here:

  1. The variable @CMD is created by concatenating values into it [ note the use of ISNULL(@CMD + N', ', N'') + QUOTENAME(c.name) ]

  2. The variable @ColumnInsertionList is populated similarly to the previous example.

  3. Note the use of system tables here again for getting the column configurations.

  4. Note the use of EXECUTE (@CMD) AT [MyTargetLinkedServer], which is used for executing the dynamic SQL command on a linked server (called “MyTargetLinkedServer” here).

Here’s a sample output of such a script:

Transact-SQL

USE MyTargetDB; IF OBJECT_ID('InvoiceHeader') IS NULL CREATE TABLE [InvoiceHeader]( [InvoiceId] int NOT NULL, [SalesPerson] nvarchar(MAX) NULL, [SalesPerson] sysname NULL, [Job] nvarchar(MAX) NULL, [Job] sysname NULL, [ShippingMethod] nvarchar(MAX) NULL, [ShippingMethod] sysname NULL, [ShippingTerms] nvarchar(MAX) NULL, [ShippingTerms] sysname NULL, [DeliveryDate] datetime NULL, [PaymentTerms] nvarchar(MAX) NULL, [PaymentTerms] sysname NULL, [DueDate] datetime NULL, [InvoiceNumber] nvarchar(50) NULL, [InvoiceNumber] sysname NULL, [InvoiceDate] nvarchar(50) NULL, [InvoiceDate] sysname NULL, [CustomerID] nvarchar(50) NULL, [CustomerID] sysname NULL, [ExpireDate] nvarchar(50) NULL, [ExpireDate] sysname NULL );

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

USE MyTargetDB;

IF OBJECT_ID('InvoiceHeader') IS NULL

CREATE TABLE [InvoiceHeader](

[InvoiceId] int NOT NULL,

[SalesPerson] nvarchar(MAX) NULL,

[SalesPerson] sysname NULL,

[Job] nvarchar(MAX) NULL,

[Job] sysname NULL,

[ShippingMethod] nvarchar(MAX) NULL,

[ShippingMethod] sysname NULL,

[ShippingTerms] nvarchar(MAX) NULL,

[ShippingTerms] sysname NULL,

[DeliveryDate] datetime NULL,

[PaymentTerms] nvarchar(MAX) NULL,

[PaymentTerms] sysname NULL,

[DueDate] datetime NULL,

[InvoiceNumber] nvarchar(50) NULL,

[InvoiceNumber] sysname NULL,

[InvoiceDate] nvarchar(50) NULL,

[InvoiceDate] sysname NULL,

[CustomerID] nvarchar(50) NULL,

[CustomerID] sysname NULL,

[ExpireDate] nvarchar(50) NULL,

[ExpireDate] sysname NULL

);

See how SQL Server generated the creation script for me? And I can do this for as many tables as I want!

Next up, is building the incremental copy script:

Transact-SQL

-- Copy data into table by Chunks SET @CMD = N' DECLARE @ChunkStart ' + @PKColumnType + N' = 0 DECLARE @ChunkEnd ' + @PKColumnType + N' DECLARE @ChunkFinish ' + @PKColumnType + N' SELECT @ChunkStart = ISNULL(MAX(' + @PKColumn + N'), 0) FROM [MyTargetLinkedServer].MyTargetDB.dbo.' + QUOTENAME(@Table) + N' SELECT @ChunkFinish = MAX(' + @PKColumn + N'), @ChunkStart = CASE WHEN @ChunkStart = 0 THEN MIN(' + @PKColumn + N')-1 ELSE @ChunkStart END FROM ' + QUOTENAME(@Table) + N' WHILE @ChunkStart < @ChunkFinish BEGIN SET @ChunkEnd = @ChunkStart + @ChunkInterval; INSERT INTO [MyTargetLinkedServer].MyTargetDB.dbo.' + QUOTENAME(@Table) + N'(' + @ColumnInsertionList + N') SELECT ' + @ColumnInsertionList + N' FROM ' + QUOTENAME(@Table) + N' WHERE ' + @PKColumn + N' > @ChunkStart AND ' + @PKColumn + N' <= @ChunkEnd SELECT @ChunkStart = MIN(' + @PKColumn + N') FROM ' + QUOTENAME(@Table) + N' WHERE ' + @PKColumn + N' >= @ChunkEnd END ' DECLARE @Params NVARCHAR(MAX) = N'@ChunkInterval bigint' EXEC sp_executesql @CMD, @Params, 10000

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

-- Copy data into table by Chunks

SET @CMD = N'

DECLARE @ChunkStart ' + @PKColumnType + N' = 0

DECLARE @ChunkEnd ' + @PKColumnType + N'

DECLARE @ChunkFinish ' + @PKColumnType + N'

SELECT @ChunkStart = ISNULL(MAX(' + @PKColumn + N'), 0)

FROM [MyTargetLinkedServer].MyTargetDB.dbo.' + QUOTENAME(@Table) + N'

SELECT @ChunkFinish = MAX(' + @PKColumn + N'), @ChunkStart = CASE WHEN @ChunkStart = 0 THEN MIN(' + @PKColumn + N')-1 ELSE @ChunkStart END

FROM ' + QUOTENAME(@Table) + N'

WHILE @ChunkStart < @ChunkFinish

BEGIN

SET @ChunkEnd = @ChunkStart + @ChunkInterval;

INSERT INTO [MyTargetLinkedServer].MyTargetDB.dbo.' + QUOTENAME(@Table) + N'(' + @ColumnInsertionList + N')

SELECT ' + @ColumnInsertionList + N'

FROM ' + QUOTENAME(@Table) + N'

WHERE

' + @PKColumn + N' > @ChunkStart

AND ' + @PKColumn + N' <= @ChunkEnd

SELECT @ChunkStart = MIN(' + @PKColumn + N')

FROM ' + QUOTENAME(@Table) + N'

WHERE ' + @PKColumn + N' >= @ChunkEnd

END

'

DECLARE @Params NVARCHAR(MAX) = N'@ChunkInterval bigint'

EXEC sp_executesql @CMD, @Params, 10000

This is a rather standard way of incrementally copying data from one table to another using an incremental primary key column.

Note that this dynamic script is built using the @ColumnInsertionList  which we have prepared earlier using concatenation and system tables, and the @PKColumn  variable which we got from the cursor.

Here’s a sample output of such a script:

Transact-SQL

DECLARE @ChunkStart int = 0 DECLARE @ChunkEnd int DECLARE @ChunkFinish int SELECT @ChunkStart = ISNULL(MAX([InvoiceId]), 0) FROM [MyTargetLinkedServer].MyTargetDB.dbo.[InvoiceHeader] SELECT @ChunkFinish = MAX([InvoiceId]), @ChunkStart = CASE WHEN @ChunkStart = 0 THEN MIN([InvoiceId])-1 ELSE @ChunkStart END FROM [InvoiceHeader] WHILE @ChunkStart < @ChunkFinish BEGIN SET @ChunkEnd = @ChunkStart + @ChunkInterval; INSERT INTO [MyTargetLinkedServer].MyTargetDB.dbo.[InvoiceHeader]([InvoiceId], [SalesPerson], [SalesPerson], [Job], [Job], [ShippingMethod], [ShippingMethod], [ShippingTerms], [ShippingTerms], [DeliveryDate], [PaymentTerms], [PaymentTerms], [DueDate], [InvoiceNumber], [InvoiceNumber], [InvoiceDate], [InvoiceDate], [CustomerID], [CustomerID], [ExpireDate], [ExpireDate]) SELECT [InvoiceId], [SalesPerson], [SalesPerson], [Job], [Job], [ShippingMethod], [ShippingMethod], [ShippingTerms], [ShippingTerms], [DeliveryDate], [PaymentTerms], [PaymentTerms], [DueDate], [InvoiceNumber], [InvoiceNumber], [InvoiceDate], [InvoiceDate], [CustomerID], [CustomerID], [ExpireDate], [ExpireDate] FROM [InvoiceHeader] WHERE [InvoiceId] > @ChunkStart AND [InvoiceId] <= @ChunkEnd SELECT @ChunkStart = MIN([InvoiceId]) FROM [InvoiceHeader] WHERE [InvoiceId] >= @ChunkEnd END

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

DECLARE @ChunkStart int = 0

DECLARE @ChunkEnd int

DECLARE @ChunkFinish int

SELECT @ChunkStart = ISNULL(MAX([InvoiceId]), 0)

FROM [MyTargetLinkedServer].MyTargetDB.dbo.[InvoiceHeader]

SELECT @ChunkFinish = MAX([InvoiceId]), @ChunkStart = CASE WHEN @ChunkStart = 0 THEN MIN([InvoiceId])-1 ELSE @ChunkStart END

FROM [InvoiceHeader]

WHILE @ChunkStart < @ChunkFinish

BEGIN

SET @ChunkEnd = @ChunkStart + @ChunkInterval;

INSERT INTO [MyTargetLinkedServer].MyTargetDB.dbo.[InvoiceHeader]([InvoiceId], [SalesPerson], [SalesPerson], [Job], [Job], [ShippingMethod], [ShippingMethod], [ShippingTerms], [ShippingTerms], [DeliveryDate], [PaymentTerms], [PaymentTerms], [DueDate], [InvoiceNumber], [InvoiceNumber], [InvoiceDate], [InvoiceDate], [CustomerID], [CustomerID], [ExpireDate], [ExpireDate])

SELECT [InvoiceId], [SalesPerson], [SalesPerson], [Job], [Job], [ShippingMethod], [ShippingMethod], [ShippingTerms], [ShippingTerms], [DeliveryDate], [PaymentTerms], [PaymentTerms], [DueDate], [InvoiceNumber], [InvoiceNumber], [InvoiceDate], [InvoiceDate], [CustomerID], [CustomerID], [ExpireDate], [ExpireDate]

FROM [InvoiceHeader]

WHERE

[InvoiceId] > @ChunkStart

AND [InvoiceId] <= @ChunkEnd

SELECT @ChunkStart = MIN([InvoiceId])

FROM [InvoiceHeader]

WHERE [InvoiceId] >= @ChunkEnd

END

Once again, SQL Server generated the full script for me and it will be automatically done per each table, and all I had to write is one template!

Next, we simply close the loop and clean up the cursor object:

Transact-SQL

FETCH NEXT FROM Cur INTO @Table, @PKColumn, @PKColumnType END CLOSE Cur DEALLOCATE Cur

1

2

3

4

5

FETCH NEXT FROM Cur INTO @Table, @PKColumn, @PKColumnType

END

CLOSE Cur

DEALLOCATE Cur

Nothing to explain here if you’re already familiar with cursors.

That’s it! You can download the full sample script here:


Conclusion

In this case study I wrote about 100 lines of code which generated for me about a thousand lines of code that I didn’t need to write and manage on my own. The benefit here is HUGE.

This is just a couple examples of something you can do to make SQL Server write tons of lines of code for you.

The possibilities are truly endless. You can prepare such ‘templates’ for yourself that generate and execute code, and use it and re-use it on several environments, almost without any changes to your scripts (because it’ll be automatically generated using the database metadata).

Microsoft SQL Server exposes for you a lot of different system tables (catalog views) which you can leverage for your own benefit. They are all documented at SQL Server Books Online.

Resources

Here are a bunch of resources for reading up on available system tables and catalog views:

And here are some resources on the various T-SQL methodologies that can help us in building dynamic SQL scripts:

And here are some of my other blog posts also talking about similar dynamic SQL methodologies:

Got comments? Questions? Write them in the comments section below!

0 comments

STAY IN TOUCH

Get New posts delivered straight to your inbox

Thank you for subscribing!

bottom of page