Today I was writing a stored procedure and had the need to create a dynamic PIVOT query in T-SQL and put the results in a temporary table. Not being a regular PIVOT user I figured this was a well known and solved issue.
Surprisingly, my first google hits provided no relief. Some posts I found mentioned OPENQUERY or OPENROWSET or using global temp tables. There were posts about dynamic pivots, but the temp table issue was unresolved. sp_executesql opens another session, temp tables created within it fall out of scope, you can’t create the temp table in advance because you don’t know all the columns, etc.
I thought about it for a while and realized the solution, just create the temp table with one column, and then dynamically alter it. A working example is below.
This example will be all temp tables so its easy to run, but a true real world example would have the sales and manufacturer info in persistent tables, primary keys, foreign keys, indexes etc. etc.
First lets create two tables to report on. Most PIVOT examples use some arbitrary piece of data like a name or date text as the column names. Not a fan of this myself. The source of my data will be three simple tables, #Manufacturers, #Years and #Sales
Note: Apparently the T-SQL syntax highlighter I use doesn’t like NVARCHAR(MAX), sp_executesql, or QUOTENAME(), hence the lame coloring for now.
CREATE TABLE #Manufacturers ( ManufacturerID INT PRIMARY KEY, Name VARCHAR(128) ) INSERT INTO #Manufacturers (ManufacturerID, Name) VALUES (1,'Dell') INSERT INTO #Manufacturers (ManufacturerID, Name) VALUES (2,'Lenovo') INSERT INTO #Manufacturers (ManufacturerID, Name) VALUES (3,'HP')
Next #Years. Now Year is actually one of those names that actually works ok as a column name if you are doing a single pivot. 2014 or 2015 as the column name is self explanatory and easy to deal with programatically. However, I will use the #Years table to show how to rename the column header dynamically.
CREATE TABLE #Years (YearID INT, Description VARCHAR(128)) GO INSERT INTO #Years (YearID, Description) VALUES (1, '2014') INSERT INTO #Years (YearID, Description) VALUES (2, '2015') GO
Finally #Sales. Note the revenue figures, these are semi-made up but I believe to be in the ballpark range. Yes it is a lot of money! Here is our #Sales table
CREATE TABLE #Sales (ManufacturerID INT, YearID INT,Revenue MONEY) GO INSERT INTO #Sales (ManufacturerID, YearID, Revenue) VALUES(1,2,59000000000) INSERT INTO #Sales (ManufacturerID, YearID, Revenue) VALUES(2,2,46000000000) INSERT INTO #Sales (ManufacturerID, YearID, Revenue) VALUES(3,2,111500000000) INSERT INTO #Sales (ManufacturerID, YearID, Revenue) VALUES(1,1,55000000000) INSERT INTO #Sales (ManufacturerID, YearID, Revenue) VALUES(2,1,42000000000) INSERT INTO #Sales (ManufacturerID, YearID, Revenue) VALUES(3,1,101500000000) GO
Next we need to build some strings for the columns to be used in our dynamic SQL statements
DECLARE @SQL AS NVARCHAR(MAX) DECLARE @PivotColumnName AS NVARCHAR(MAX) DECLARE @TempTableColumnName AS NVARCHAR(MAX) DECLARE @AlterTempTable AS NVARCHAR(MAX) --get delimited column names for various SQL statements below SELECT -- column names for pivot @PivotColumnName= ISNULL(@PivotColumnName + N',',N'') + QUOTENAME(CONVERT(NVARCHAR(10),YearID)), -- column names for insert into temp table @TempTableColumnName = ISNULL(@TempTableColumnName + N',',N'') + QUOTENAME('Y' + CONVERT(NVARCHAR(10),YearID)), -- column names for alteration of temp table @AlterTempTable = ISNULL(@AlterTempTable + N',',N'') + QUOTENAME('Y' + CONVERT(NVARCHAR(10),YearID)) + ' MONEY' FROM (SELECT DISTINCT [YearID] FROM #Sales) AS Sales
I probably could use a single string rather than three, and do some string replacements in every usage, but I figured the above is the clearest. In this case I prefix the YearID with a ‘Y’.
Next, create our Pivot temp table
CREATE TABLE #Pivot ( ManufacturerID INT ) -- Thats it! Because the following step will flesh it out. SET @SQL = 'ALTER TABLE #Pivot ADD ' + @AlterTempTable EXEC sp_executesql @SQL
Note: If for some reason you did not know any field names in advance, you could create the temp table with a IDENTITY column that you just ignore, like
/* CREATE TABLE #Pivot ( IgnoreMe INT IDENTITY(1,1) ) */
Now we can just execute the dynamic PIVOT
--execute the dynamic PIVOT query into the temp table SET @SQL = N' INSERT INTO #Pivot (ManufacturerID, ' + @TempTableColumnName + ') SELECT ManufacturerID, ' + @PivotColumnName + ' FROM #Sales S PIVOT(SUM(Revenue) FOR S.YearID IN (' + @PivotColumnName + ')) AS PivotTable' EXEC sp_executesql @SQL
Now we can return our results like so
SELECT M.Name, P.* FROM #Manufacturers M INNER JOIN #Pivot P ON M.ManufacturerID = P.ManufacturerID
This results in
Finally, placing the a leading character in the column name may seem like an unnecessary step, but if you had two dynamic pivots in your stored procedure, say one has CategoryID as the column header and another had RegionID, or another key field in the database as the header, how to tell them apart if you returned as a single joined result? A clash is a possibility if CategoryID=1 exists and you also have RegionID =1. Using a C and R prefix in that case will prevent clashes when joining the temp tables.