Showing posts with label sql server. Show all posts
Showing posts with label sql server. Show all posts

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.

Friday, December 30, 2011

Concatenate values of a column to display in a row (also use of cross apply)

Hi Everyone,
i will be demonstrating how to use xml path to concatenate column values into a single value. we will also use cross apply to get multiple values(more than one column) to be selected in a select statement.

For demonstrating the above, lets consider the following scenario

  1. i have a few boxes
  2. i have a few gifts (some new, some old ... ahm... age old regifting)

i need to record information about each box, need to store data about each gift ( also an indicator marking if it is a new gift or an unwanted old gift that i want to give away) . i also need to store information about which box holds which gifts.

lets look at the structure of the tables

CREATE TABLE MyBoxes
(
BoxID INT IDENTITY(1,1) CONSTRAINT MyBoxes_BoxID PRIMARY KEY
, BoxName VARCHAR(20) NOT NULL
)
GO
CREATE TABLE MyGifts
(
GiftID INT IDENTITY(1,1) CONSTRAINT MyGifts_GiftID PRIMARY KEY
, GiftName VARCHAR(50) NOT NULL
)
GO



now lets populate these two tables with some data

INSERT INTO MyBoxes(BoxName)
SELECT TOP 10000 'B' + CAST(ROW_NUMBER() OVER(ORDER BY C1.NAME) AS VARCHAR(20))
FROM sys.columns C1
CROSS JOIN sys.columns C2

INSERT INTO MyGifts(GiftName)
VALUES('G1'), ('G2'), ('G3'), ('G4'), ('G5')


The above statements would have inserted 1000 boxes into MyBoxes table and 5 gift types into MyGifts table.

Now we need a table to store information on which gift (of a perticular type) is strored in which box and also if the gift is new or not.

CREATE TABLE BoxedGifts
(
BoxID INT
CONSTRAINT FK_BoxedGifts_BoxID_MyBoxes_BoxID
FOREIGN KEY REFERENCES MyBoxes(BoxID)
, GiftID INT
CONSTRAINT FK_BoxedGifts_GiftID_MyGifts_GiftID
FOREIGN KEY REFERENCES MyGifts(GiftID)
, NewGift BIT
, CONSTRAINT PK_BoxedGifts
PRIMARY KEY
(
BoxID
, GiftID
)
)


lets populate the above table with some data

INSERT INTO BoxedGifts(BoxID, GiftID, NewGift)
SELECT B.BoxID, G.GiftID, G.NewGift
FROM MyBoxes B
CROSS APPLY
(
SELECT TOP (B.BoxID % 5) GiftID, CAST( (GiftID % 3) AS BIT) AS NewGift
FROM MyGifts
) G


i have used "cross apply" and "top" to put gift items in such a way that for every five boxes, the pattern of type and number of gifts will repeat.

Now i need to get back the information in the following way:
i want to display each box along with the new gifts and old gifts i have put in it.

this can be accomplished in the following way :

SELECT BoxID
, ISNULL((
SELECT CAST(GiftID AS VARCHAR(10)) + ','
FROM BoxedGifts BG
WHERE
BG.BoxID = B.BoxID
AND NewGift = 0
FOR XML PATH('')
), '') AS OldGifts
, ISNULL((
SELECT CAST(GiftID AS VARCHAR(10)) + ','
FROM BoxedGifts BG
WHERE
BG.BoxID = B.BoxID
AND NewGift = 1
FOR XML PATH('')
), '') AS NewGifts

FROM MyBoxes B

the above query will give us the required output. lets now look at how the same can be accomplished using a cross apply.
SELECT BoxID
, G.OldGifts
, G.NewGifts
FROM MyBoxes B
CROSS APPLY (
SELECT ISNULL((
SELECT CAST(GiftID AS VARCHAR(10)) + ','
FROM BoxedGifts BG
WHERE
BG.BoxID = B.BoxID
AND NewGift = 0
FOR XML PATH('')
), '') AS OldGifts
, ISNULL((
SELECT CAST(GiftID AS VARCHAR(10)) + ','
FROM BoxedGifts BG
WHERE
BG.BoxID = B.BoxID
AND NewGift = 1
FOR XML PATH('')
), '') AS NewGifts
) G



result of running above queries can be seen in the following image:



hope you find this article informative.