Monday, April 23, 2012

Dynamically create a temp table with variable number of columns

While programming with sql we usually come across a situation where we want to pass data to stored procedure where the data is actually a string concatenation of different values and the stored procedure would construct a table out of this string to perform relational operations. Let’s look at one such scenario.
Scenario:
                Say you have the following tables
CREATE TABLE [dbo].[Product](
ProductID INT IDENTITY(1,1)  PRIMARY KEY
,  Name VARCHAR(200)
, Description VARCHAR(MAX)
)

CREATE TABLE [dbo].[Part](
      PartID INT IDENTITY(1,1) PRIMARY KEY
      , Name VARCHAR(30)     
)

CREATE TABLE [dbo].[ProductPart](
      ProductID INT FOREIGN KEY REFERENCES Product(ProductID)
      , PartID INT FOREIGN KEY REFERENCES Part(PartID)
)

Let’s populate these tables with test data
INSERT INTO Product(Name, Description)
VALUES('Car', 'it will take you to work')


INSERT INTO Part(Name)
VALUES('Wheel'),('Door'), ('Boot'), ('Monitor')
These tables will now have the following data

Let us suppose you want to store the data into ProductPart table. You want to store the information where you want to say a car has wheels, Doors and a Boot.

Traditionally you will have to write a stored procedure where it will take partID and ProductID as parameters and saves it into the ProductPart table one at a time.
It would look like this
CREATE PROCEDURE [dbo].[ProductPartSave]
(
      @ProductID INT
      , @PartID INT
)
AS
INSERT INTO [ProductPart]
SELECT @ProductID, @PartID

It will work fine. Except that you will have to make three calls to the database in our case.
There is one more way. You can create a stored procedure that will take two parameters. The first parameter would be the @ProductID and the second @PartIDs.



Definition of our new stored procedure is

CREATE PROCEDURE [dbo].[ProductMultiplePartsSave]
(
      @ProductID INT
      , @PartIDs VARCHAR(MAX)
)
AS

DECLARE @Parts TABLE(PartID INT)

INSERT INTO @Parts(PartID)
EXEC [GetDynamicTable] @DataList = @PartIDs, @ColumnSeperator = ',', @RowSeperator = '|', @NumberOfColumns = '1'

INSERT INTO [ProductPart](ProductID, PartID)
SELECT @ProductID
      , PartID
FROM @Parts

As you can see, we have a used a stored procedure called [dbo].[GetDynamicTable] to get a table from string.
The code for this sp is

CREATE PROCEDURE [dbo].[GetDynamicTable]
(
      @DataList AS VARCHAR(MAX),
      @ColumnSeperator AS CHAR(1),
      @RowSeperator AS CHAR(1),
      @NumberOfColumns INT
)
AS   
DECLARE @DynamicTableSQL NVARCHAR(MAX)
DECLARE @ColumnCount INT
DECLARE @Row NVARCHAR(MAX)


SET @DynamicTableSQL = 'DECLARE @DynamicTable TABLE('
SET @ColumnCount = 1
WHILE @ColumnCount <= @NumberOfColumns
BEGIN
      SET @DynamicTableSQL = @DynamicTableSQL + 'Column' + CAST(@ColumnCount AS VARCHAR(10))+ ' VARCHAR(MAX) ,'
      SET @ColumnCount = @ColumnCount + 1
END
SET @DynamicTableSQL = SUBSTRING(@DynamicTableSQL, 0, LEN(@DynamicTableSQL))
SET @DynamicTableSQL = @DynamicTableSQL + ') '

SET @DataList = REPLACE(@DataList, '''', '''''')
     
SET @DynamicTableSQL = @DynamicTableSQL + ' INSERT INTO @DynamicTable '
     
SELECT @DataList = @DataList + @RowSeperator

DECLARE @PosA INT, @PosB INT, @PosC INT

SELECT @PosA = 1
SELECT @PosC = CHARINDEX(@RowSeperator, @DataList, @PosA)

SET @Row = SUBSTRING(@DataList, @PosA, @PosC - @PosA)      

IF (LEN(@Row) > 0)
BEGIN
      SET @Row = REPLACE( @Row, @ColumnSeperator, ''',''')
      SET @DynamicTableSQL = @DynamicTableSQL + 'SELECT ''' + @Row + ''' '   
END

SELECT @PosA = @PosC + 1
SELECT @PosC = CHARINDEX(@RowSeperator, @DataList, @PosA)


WHILE @PosC > 0
BEGIN
     
      SET @Row = SUBSTRING(@DataList, @PosA, @PosC - @PosA)
      IF (LEN(@Row) > 0)
      BEGIN
            SET @DynamicTableSQL = @DynamicTableSQL + ' UNION ALL '
           
            SET @Row = REPLACE( @Row, @ColumnSeperator, ''',''')
            SET @DynamicTableSQL = @DynamicTableSQL + 'SELECT ''' + @Row + ''' '               
      END
     
      SELECT @PosA = @PosC + 1
      SELECT @PosC = CHARINDEX(@RowSeperator, @DataList, @PosA)
END
     
SET @DynamicTableSQL = @DynamicTableSQL + ' SELECT * FROM @DynamicTable '

EXEC SP_EXECUTESQL @DynamicTableSQL

The above stored procedure takes four parameters
1.       @DataList a string containing the concatenated data
2.  @ColumnSeperator a character which is the column delimiter
3.  @RowSeperator a character which is the row delimiter
4.       @NumberOfColumns – number of columns the dynamic table should have

Let’s first see how we can insert values into ProductPart table using our new stored procedures.

As you can see, we inserted multiple rows into ProductPart table with just one call to the [ProductMultiplePartsSave] stored procedure.


[dbo].[GetDynamicTable] is very powerful in terms of creating temp tables on the fly with just change of parameters. Let’s look at a few different uses of this sp:
In the following image you can see how it can be used to get a two column table

In the following image you can see it returning a table with 5 columns

I hope this article was informative and please don’t hesitate to comment or ask questions.

8 comments:

  1. Sounds handy in terms of flexibility. But doesn't this below statement cause a performance issue due to recompilation of the statement, because it's always dynamic?

    EXEC SP_EXECUTESQL @DynamicTableSQL

    ReplyDelete
    Replies
    1. Hi Daryl,
      unfortunately there is no other way (that i know of) for creating a table with variable number of columns.

      the work around would be :
      create a stored procedure /function for every column numbers.
      for example:-
      ReturnTableWith1Column, ReturnTableWith2Columns, ReturnTableWith3Columns, etc.
      it might get a bit tedious though.

      thanks,
      Khaja.

      Delete
    2. Haha I assumed that there wasn't a work around, I was just curious about the performance.

      I figured this is ok if it's called every now and again, but not for example on each request or something like that.

      And yes I realised that you could define functions like that. If I remember correctly iD already had a few functions that returned a different variety of columns. I am guessing that you needed to define another one but instead you decided to come up with this solution.

      I guess you just need to make sure that people use it wisely if it's slow.

      Delete
    3. The performance shouldn't be that much of a problem as all its doing is creating a table variable and inserting items into it and returning the rows.

      Delete
    4. Actually Daryl i think i can do one better, i might be able to get rid of table variable from the dynamic sql. i will post the updated query once i get some time.

      Delete
    5. Here is the new version of stored procedure


      CREATE PROCEDURE [dbo].[GetDynamicTable]
      (
      @DataList AS VARCHAR(MAX),
      @ColumnSeperator AS CHAR(1),
      @RowSeperator AS CHAR(1)
      )
      AS
      DECLARE @DynamicTableSQL NVARCHAR(MAX)
      DECLARE @ColumnCount INT
      DECLARE @Row NVARCHAR(MAX)


      SET @DynamicTableSQL = ''

      SELECT @DataList = @DataList + @RowSeperator

      DECLARE @PosA INT, @PosB INT, @PosC INT

      SELECT @PosA = 1
      SELECT @PosC = CHARINDEX(@RowSeperator, @DataList, @PosA)

      SET @Row = SUBSTRING(@DataList, @PosA, @PosC - @PosA)

      IF (LEN(@Row) > 0)
      BEGIN
      SET @Row = REPLACE( @Row, @ColumnSeperator, ''',''')
      SET @DynamicTableSQL = @DynamicTableSQL + 'SELECT ''' + @Row + ''' '
      END

      SELECT @PosA = @PosC + 1
      SELECT @PosC = CHARINDEX(@RowSeperator, @DataList, @PosA)


      WHILE @PosC > 0
      BEGIN

      SET @Row = SUBSTRING(@DataList, @PosA, @PosC - @PosA)
      IF (LEN(@Row) > 0)
      BEGIN
      SET @DynamicTableSQL = @DynamicTableSQL + ' UNION ALL '

      SET @Row = REPLACE( @Row, @ColumnSeperator, ''',''')
      SET @DynamicTableSQL = @DynamicTableSQL + 'SELECT ''' + @Row + ''' '
      END

      SELECT @PosA = @PosC + 1
      SELECT @PosC = CHARINDEX(@RowSeperator, @DataList, @PosA)
      END


      EXEC SP_EXECUTESQL @DynamicTableSQL

      Delete
    6. i fired up the profiler and checked for sp:Recompile and sql:stmtRecompile. then ran the execution of our sp and nothing got registered.
      So it doesnt' cause any statement recompiles.

      Delete
    7. OK I had one more concern with this. And that is the caching of execution plans. From what I could see initially, using this approach could have caused an execution plan to be estimated every time the stored procedure is executed as it was a dynamic query.

      But after reading the article below it looks like sp_executesql tries its best to resolve this problem.

      http://ayyanar.blogspot.com.au/2007/11/performance-difference-between-exec-and.html

      Delete