Robyn Page and Phil Factor
Crosstab Pivot-table Workbench
22 July 2007

There comes a time with many Database Developers charged with doing management reports when the process of doing it properly gets very tedious. By 'doing it properly', I mean the 'best practice' of having to do the basic reporting in SQL and relying on a front-end application to do the presentation. This is particularly true where the management want simple aggregate reports, or 'Pivot-table' reports. Presentation is so closely tied with the data that splitting the process can sometimes lead to more problems than it solves. Of course, we have Reporting Services, Analysis Services and other external tools, but there are times when a simple solution based in TSQL has the upper hand.

Anyone who was weaned on Excel knows that these pivot tables are dead simple. You select your data, bang the button and with a bit of dragging and dropping, there it is. Why, they ask, is it so hard to get it out of the database? Why so hard to make changes?

What they want to see is something like this (using NorthWind so those stuck with SQL 2000 can join in)

No. Sales per year

  1996 1997 1998 Total
Margaret Peacock 31 81 44 156
Janet Leverling 18 71 38 127
Nancy Davolio 26 55 42 123
Laura Callahan 19 54 31 104
Andrew Fuller 16 41 39 96
Robert King 11 36 25 72
Michael Suyama 15 33 19 67
Anne Dodsworth 5 19 19 43
Steven Buchanan 11 18 13 42
Sum 152 408 270 830

Now, you'll notice that we've taken a bit of trouble to add some formatting. In the average business, they're fussy about such things as the alignment of numbers and the clear delineation of totals, and summary lines. It also is easier on the eye. It is therefore handy to format pivot table reports in HTML. We can send them via email them, straight from SQL Server, all in their correct formatting, or put the reports on an intranet and update them daily or hourly. With the contents of this workbench we show you how it is done, and how easy it is to do, all without a .NET programmer in sight!

However, first things first, we must first show you how to do a crosstab, or pivot table in Transact SQL

Crosstabs and Pivot tables

The basic code to do the report is pretty simple.

SELECT 
  
[No. Sales per year]=CASE WHEN row IS NULL THEN 'Sum' 
                   
ELSE CONVERT(VARCHAR(80),[row]END ,
  
[1996] =SUMCASE col WHEN '1996' THEN data ELSE END ),
  
[1997] =SUMCASE col WHEN '1997' THEN data ELSE END ),
  
[1998] =SUMCASE col WHEN '1998' THEN data ELSE END ),
  
[Total]SUMdata )
FROM 
   
(SELECT [row]=firstname+' '+lastname
           
[col]=YEAR(OrderDate), 
           
[data]=COUNT(*)
    
FROM Employees INNER JOIN Orders 
    
ON (Employees.EmployeeID=Orders.EmployeeID
    
GROUP BY firstname+' '+lastnameYEAR(OrderDate)
    )
f
GROUP BY row WITH ROLLUP
ORDER BY GROUPING(row),total DESC

You'll notice that the years are hard-coded into the column headings, which are a time bomb waiting to happen. You'll also realize that the all-important formatting is missing. The structure of the query seems slightly more complicated than necessary, but you'll see why soon. The PIVOT operator in SQL Server 2005 makes it rather easier but we wanted to make this relevant to the SQL Server 2000 users too

For any sort of portable solution that will work on SQL Server 2000, dynamic SQL is the traditional solution. Basically, the stored procedure generates the tedious code and then executes it. Why, one wonders. This is because there are a number of tweaks that have to be made, such as the order of the columns, and rows. After all, wouldn't someone want the report ordered by the number of sales of the salesman rather than just alphabetic order? The same basic query may generate a lot of different aggregations. Pretty soon, some sort of automation will be required.

Keith Fletcher contributed to Simple-Talk the ingenious but complex stored procedure that did cross-tabs in his excellent article Creating cross tab queries and pivot tables in SQL. Because we were awed by its grandeur, we didn't initially want to add our own contribution. However, we had two another objectives, firstly to show how easy the technique can be, and also because we wanted to do more, encouraging you to try things out, and secondly, because we wanted to show how one might mark up the presentation of the crosstab.

Here is a stored procedure that does the trick, along with some examples using NorthWind. (if you are stuck with SQL Server 2000, make the Varchar(MAX)s into Varchar(8000) and don't be too ambitious with the complexity of your crosstabs!)

-------------------------------------------------------------------------
CREATE PROCEDURE spDynamicCrossTab

@RowValue VARCHAR(255),         --what is the SQL for the row title 
@ColValue VARCHAR(255),         --what is the SQL for the column title
@Aggregate VARCHAR(255),        --the aggregation value to go in the cells
@FromExpression VARCHAR(8000),              --the FROM, ON and WHERE clause
@colOrderValue VARCHAR (255)=NULL,            --how the columns are ordered
@Title VARCHAR(80)='_',    --the title to put in the first col of first row
@SortBy VARCHAR(255)='row asc'--what you sort the rows by (column heading)
@RowSort VARCHAR(80)=NULL,
@ReturnTheDDL INT=0,--return the SQL code rather than execute it
@Debugging INT=0    --debugging mode
/*
e.g.
Execute spDynamicCrossTab
    @RowValue='firstname+'' ''+lastname',
    @ColValue='Year(OrderDate)',
    @Aggregate= 'count(*)',
    @FromExpression='FROM Employees INNER JOIN Orders 
    ON (Employees.EmployeeID=Orders.EmployeeID)',
    @ColOrderValue='Year(OrderDate)',
   @Title ='No. Sales per year',
   @SortBy ='total desc' --what you sort the rows by (column heading)

Execute spDynamicCrossTab
    @RowValue='firstname+'' ''+lastname',
    @ColValue='DATENAME(month,orderDate)',
    @Aggregate= 'sum(subtotal)',
    @FromExpression='FROM Orders 
   INNER JOIN "Order Subtotals" 
       ON Orders.OrderID = "Order Subtotals".OrderID
   inner join employees on employees.EmployeeID =orders.EmployeeID',
    @ColOrderValue='datepart(month,orderDate)',
   @Title ='Customers orders per month '

EXECUTE spDynamicCrossTab 
    @RowValue='country',
    @ColValue='datename(quarter,orderdate)
     +case datepart(quarter,orderdate) 
         when 1 then ''st'' 
         when 2 then ''nd'' 
         when 3 then ''rd'' 
         when 4 then ''th'' end',
    @Aggregate= 'sum(subtotal)',
    @FromExpression='FROM Orders 
   INNER JOIN "Order Subtotals" 
       ON Orders.OrderID = "Order Subtotals".OrderID
  inner join customers on customers.customerID =orders.customerID',
    @ColOrderValue='datepart(quarter,orderDate)',
   @sortby='total desc',
   @Title ='value of orders per quarter'

*/
AS
SET 
nocount ON
DECLARE 
@Command NVARCHAR(MAX)
DECLARE @SQL VARCHAR(MAX)
--make sure we have sensible defaults for orders
SELECT @ColOrderValue=COALESCE(@ColOrderValue@ColValue),
   
@Sortby=COALESCE(@SortBy,@RowValue),
    
@rowsort=COALESCE(@RowSort,@RowValue)
--first construct tha SQL which is used to calculate the columns in a 
--string
SELECT @Command='select @SQL=coalesce(@SQL,''SELECT 
  ['
+@Title+']=case when row is null then ''''Sum'''' 
else convert(Varchar(80),[row]) end ,
'')+
  ''[''+convert(varchar(100),'
   
+@ColValue+')+''] =sum( CASE col WHEN ''''''+convert(varchar(100),'
   
+@ColValue+')+'''''' THEN data else 0 END ),
'' '
+@FromExpression+'
GROUP BY '
+@ColValue+'
order by max('
+@ColorderValue+')'
--Now we execute the string to obtain the SQL that we will use for the
--crosstab query
EXECUTE sp_ExecuteSQL @command,N'@SQL VARCHAR(MAX) OUTPUT',@SQL OUTPUT
  
IF @@error --display the string if there is an error
    
BEGIN
      RAISERROR 
'offending code was ...%s'01@command )
      
RETURN 1
    
END
IF 
@debugging <>SELECT @Command
--we now add the rest of the SQL into the string
SELECT @SQL=@SQL+'  [Total]= sum( data )
from 
   (select [row]='
+@RowValue+', 
           [col]='
+@ColValue+', 
           [data]='
+@Aggregate+',
           [sort]=max('
+@rowsort+')
 '
+@FromExpression+
    GROUP BY '
+@RowValue+', '+@ColValue+'
)f
group by row with rollup
order by grouping(row),'
+@Sortby
--and execute it
IF @ReturnTheDDL<>SELECT @SQL ELSE EXECUTE (@SQL)
  
IF @@error 
    
BEGIN
      RAISERROR 
'offending code was ...%s'01@sql )
      
RETURN 1
    
END



You'll see that this is a developer's tool. It is easy to crash the procedure by putting in bad SQL. SQL Injectors would love it. No sir, this is a back-office report-generating tool. Let's try it out

EXECUTE spDynamicCrossTab
    
@RowValue='ProductName',
    
@ColValue='Year(OrderDate)',
    
@Aggregate'ROUND(SUM(CONVERT(decimal(14, 2), OD.Quantity 
* ( 1 - OD.Discount ) * OD.UnitPrice)),0)'
,
    
@FromExpression='FROM    [Order Details] OD,
        Orders O,
        Products P,
        Categories C
where   OD.OrderID = O.OrderID 
AND OD.ProductID = P.ProductID 
AND P.CategoryID = C.CategoryID'
,
   
@Title ='Customers total orders per year'

-- change the line...     @RowValue='ProductName', 
--             ...to     @RowValue='CategoryName',
-- and see what happens!
/*-------------------------------------------------------------------

*/
--
-- add the row ...
-- @SortBy ='total desc', --what you sort the rows by (column heading)
-- before the @Title ='Customers total orders per year'-- Neat Huh?
--now change 
--    @RowValue='CategoryName',
--    @ColValue='Year(OrderDate)',
--to
--    @ColValue='CategoryName',
--    @RowValue='Year(OrderDate)',
--Instant Rotation!
--Now try this, and notice how we get the columns and rows in the right order

EXECUTE spDynamicCrossTab
    
@colValue='DATENAME(year,orderDate)',
    
@rowValue='DATENAME(month,orderDate)',
    
@Aggregate'sum(subtotal)',
   
@Rowsort='DATEpart(month,orderDate)',
    
@FromExpression='FROM Orders 
   INNER JOIN "Order Subtotals" 
       ON Orders.OrderID = "Order Subtotals".OrderID
   inner join employees on employees.EmployeeID =orders.EmployeeID'
,
    
@ColOrderValue='datepart(year,orderDate)',
   
@Title ='Customers orders per month ',
   
@sortby='max(sort) asc'
/*-------------------------------------------------------------------

HTML Crosstabs

Why bother with this when the Internet abounds with such Cross tab or pivot-table procedures? This is because we are going to take it one stage further so that, instead of just producing a resultset, we want to show how to produce HTML to produce the chart like the one at the beginning of the article.

To reproduce this...

value of orders per quarter

  1st 2nd 3rd 4th Total
USA $81364.94 $50525.40 $58047.56 $55646.73 $245584.63
Germany $66823.20 $64681.22 $42550.91 $56229.29 $230284.62
Austria $32357.82 $37346.79 $17383.60 $40915.63 $128003.84
Brazil $47027.15 $12127.25 $22537.77 $25233.60 $106925.77
France $31085.27 $11225.97 $14407.11 $24639.96 $81358.31
UK $21302.05 $17061.85 $2292.70 $18314.72 $58971.32
Venezuela $21186.40 $11991.60 $12098.75 $11533.89 $56810.64
Sweden $13627.62 $18234.86 $8718.31 $13914.35 $54495.14
Canada $22746.94 $7094.48 $9225.70 $11129.18 $50196.30
Ireland $20500.54 $8291.50 $11376.50 $9811.36 $49979.90
Belgium $19410.67 $3728.48 $7740.70 $2945.00 $33824.85
Denmark $17049.90 $2007.79 $2127.25 $11476.08 $32661.02
Switzerland $4454.02 $10928.70 $7714.06 $8595.88 $31692.66
Mexico $3938.00 $12432.71 $4616.17 $2595.20 $23582.08
Finland $5532.80 $5791.20 $2884.41 $4601.64 $18810.05
Spain $7329.30 $1875.80 $4633.25 $4144.85 $17983.20
Italy $7082.09 $2836.10 $2484.37 $3367.60 $15770.16
Portugal $3335.79 $4311.20 $1519.24 $2306.14 $11472.37
Argentina $6684.10 $716.50 $0.00 $718.50 $8119.10
Norway $3354.40 $822.35 $500.00 $1058.40 $5735.15
Poland $587.50 $1277.60 $808.00 $858.85 $3531.95
Sum $436780.50 $285309.35 $233666.36 $310036.85 $1265793.06

...We need code like this (the first example in the body of the code was used to generate this Pivot-table)…

-------------------------------------------------------------------------
CREATE PROCEDURE spDynamicHTMLCrossTab

@RowValue VARCHAR(255), --what is the row header
@ColValue VARCHAR(255), --what is the column header
@Aggregate VARCHAR(255), --the aggregation value
@FromExpression VARCHAR(8000), --the FROM, ON and WHERE clause
@colOrderValue VARCHAR (255)=NULL, --how the columns are ordered
@Title VARCHAR(80)='_'--the title to put in the first col of first row
@RowSort VARCHAR(80)=NULL,--any special way the rows should be sorted
@SortBy VARCHAR(255)='row asc'--what you sort the rows by (column heading)
@UnitBefore VARCHAR(10)='',--the unit that each value has before (e.g. £ or $)
@UnitAfter VARCHAR(10)='',--The unit that each value has after e.g. %
@ReturnTheDDL INT=0,--we return just the DLL
@Debugging INT=0,--we look at the intermediate code
@output VARCHAR(MAX='none' output,
@style VARCHAR(MAX)='<style type="text/css">
/*<![CDATA[*/
<!--
#MyCrosstab {
font-family: Arial, Helvetica, sans-serif; font-size:small;
}
#MyCrosstab td{font-size:small; padding: 3px 10px 2px 10px; }
#MyCrosstab td.number{ text-align: right; }
#MyCrosstab td.rowhead{ border-right: 1px dotted #828282; font-weight: bold;}
#MyCrosstab th{ font-size:small; border-bottom: 1px dotted #828282; text-align: center; }
#MyCrosstab .sum{ border-top: 2px solid #828282; }
#MyCrosstab .sumrow{ text-align: right }
#MyCrosstab .total{ border-left: 1px solid #828282; }
-->
/*]]>*/
</style>
'

/*
Declare @HTMLString varchar(max) 
EXECUTE spDynamicHTMLCrossTab 
    @RowValue='CompanyName',
    @ColValue='datename(quarter,orderdate)
     +case datepart(quarter,orderdate) 
         when 1 then ''st'' 
         when 2 then ''nd'' 
         when 3 then ''rd'' 
         when 4 then ''th'' end',
    @Aggregate= 'sum(subtotal)',
    @FromExpression='FROM Orders 
   INNER JOIN "Order Subtotals" 
       ON Orders.OrderID = "Order Subtotals".OrderID
  inner join customers on customers.customerID =orders.customerID',
    @ColOrderValue='datepart(quarter,orderDate)',
   @Unitbefore='$',
   @sortby='total desc',
   @Title ='value of orders per quarter',
   @Output=@HTMLString output
Select @HTMLString

Execute spDynamicHTMLCrossTab 
    @RowValue='firstname+'' ''+lastname', 
    @ColValue='DATENAME(year,orderDate)', 
    @Aggregate= 'sum(subtotal)', 
    @FromExpression='FROM Orders  
   INNER JOIN "Order Subtotals"  
       ON Orders.OrderID = "Order Subtotals".OrderID 
   inner join employees on employees.EmployeeID =orders.EmployeeID', 
    @ColOrderValue='datepart(year,orderDate)', 
   @Unitbefore='$',
   @sortby='total desc',
   @Title ='Revenue per salesman per year '

Execute spDynamicHTMLCrossTab
    @RowValue='firstname+'' ''+lastname',
    @ColValue='Year(OrderDate)',
    @Aggregate= 'count(*)',
    @FromExpression='FROM Employees INNER JOIN Orders 
    ON (Employees.EmployeeID=Orders.EmployeeID)',
    @ColOrderValue='Year(OrderDate)',
   @Title ='No. Sales per year',
   @SortBy ='total desc', --what you sort the rows by (column heading)
    @ReturnTheDDL =0,
    @debugging=0
*/
AS
SET 
nocount ON
DECLARE 
@Command NVARCHAR(MAX)
DECLARE @DataRows VARCHAR(MAX)
DECLARE @HeadingLines VARCHAR(8000)
--make sure we have sensible defaults for orders
SELECT @ColOrderValue=COALESCE(@ColOrderValue@ColValue),
    
@rowsort=COALESCE(@RowSort,@RowValue),
   
@Sortby=COALESCE(@SortBy,@RowValue)
--first construct tha SQL which is used to calculate the columns in a 
--string
DECLARE @StringTable TABLE
  
(
    
MyID INT IDENTITY(11),
    
string VARCHAR(8000),
    
waste numeric(19,8)
  )

SELECT 

   
@Command='Select 
 @Headinglines=coalesce(@headinglines,''<div id="MyCrosstab">
<h3>'
+@title+'</h3>
<table cellpadding="0" cellspacing="0">
<thead>
<tr class="header"><th> </th>'')+''<th>''
+convert(varchar(100),'
   
+@ColValue+') +''</th>'',
 @DataRows=coalesce(@DataRows,
''SELECT 
[string]=''''<tr>
  <td class="rowhead''''
+ case when grouping(row)<>0 then'''' sumrow'''' else '''''''' end
+''''">''''+convert(varchar(100),case when row is null 
then ''''Sum'''' else [row] end)+''''</td>
'')
 +''<td class="''''
+ case when grouping(row)<>0 then''''sum'''' else '''''''' end
+'''' number">''''+'''''+@unitBefore+'''''+convert(varchar(100),sum( CASE col WHEN ''''''
 +convert(varchar(100),'
   
+@ColValue+')
 +'''''' THEN data else 0 END ))++'''''+@unitAfter+'''''+''''</td>
''  '
+@FromExpression+'
GROUP BY '
+@ColValue+'
order by max('
+@ColorderValue+')'
--Now we execute the string to obtain the SQL that we will use for the
--crosstab query
EXECUTE sp_ExecuteSQL @command,N'@DataRows VARCHAR(MAX) OUTPUT,
  @Headinglines VARCHAR(MAX) OUTPUT'
@DataRows output,@Headinglines OUTPUT
  
IF @@error --display the string if there is an error
    
BEGIN
      RAISERROR 
'offending first-phase code was ...%s'01@command )
      
RETURN 1
    
END
IF 
@Debugging <>SELECT @Command

INSERT INTO  @StringTable(stringSELECT @Style
INSERT INTO  @StringTable(stringSELECT @Headinglines+'<th>Total</th></tr>
   </thead>
   <tbody>'
SELECT @DataRows=
@DataRows+'<td class="''
  + case when grouping(row)<>0 then''sum'' else '''' end+'' number total">''
  +'''
+@unitBefore+'''+convert(varchar(100),sum( data ))+'''+@unitAfter
  
+'''+''</td></tr>'', [total]=convert(numeric(19,8),sum( data ))
from 
   (select [row]='
+@RowValue+', 
           [col]='
+@ColValue+', 
           [data]='
+@Aggregate+',
           [sort]=max('
+@rowsort+')
 '
+@FromExpression+
    GROUP BY '
+@RowValue+', '+@ColValue+'
)f
group by row with rollup
order by grouping(row),'
+@Sortby
--and execute it
IF @ReturnTheDDL<>SELECT @DataRows ELSE
   INSERT INTO  
@StringTable(string,waste)
       
EXECUTE (@DataRows)
  
IF @@error 
    
BEGIN
      RAISERROR 
'offending second-phase code was ...%s'01@DataRows )
      
RETURN 1
    
END
INSERT INTO  
@StringTable(stringSELECT '</tbody></table></div>'

IF @Output='none' 
    
SELECT string FROM @StringTable ORDER BY MyID
ELSE 
    SELECT 
@Output=COALESCE(@Output,'')+ string 
       
FROM @StringTable 
       
ORDER BY MyID

(n.b this is the SQL Server 2005 version. The SQL Server 2000 version is included with the files you can download in the speech bubble at the top of the article)

You will have noticed a few things here.

  • The inputs are the same as the first stored procedure, spDynamicCrossTab. This means that you can try out your parameters in SSMS until you have things the way you want them and then you can just move to spDynamicHTMLCrossTab to concentrate on getting the presentation aspects as you want.
  • We have separated the style from the code. All presentation is in an inline style block. This means you can change the way the crosstab looks to your heart's content.
  • with both stored procedures, you can specify the order of both the columns and rows precisely (it is always nice to have the months, or the days of the week in the correct order!
  • you can specify the units, either before(e.g. '£' or '$') or after (e.g '%') the aggregate values
  • We provide you with an optional output variable so you can take the results and save it easily to a file, using the technique Phil described in his Blog 'Using BCP to export the contents of MAX datatypes to a file'

You can change the appearance of the crosstab simply by changing the inline style. For example, this ...

EXECUTE spDynamicHTMLCrossTab
    
@RowValue='firstname+'' ''+lastname',
    
@ColValue='Year(OrderDate)',
    
@Aggregate'count(*)',
    
@FromExpression='FROM Employees INNER JOIN Orders 
    ON (Employees.EmployeeID=Orders.EmployeeID)'
,
    
@ColOrderValue='Year(OrderDate)',
   
@Title ='No. Sales per year',
   
@SortBy ='total desc'--what you sort the rows by (column heading)
   
@Style='<style type="text/css">
/*<![CDATA[*/
<!--
#MyCrosstab {
font-family: "Times New Roman", Times, serif; font-size:small;
}
#MyCrosstab td{font-size:small; padding: 3px 10px 2px 10px; }
#MyCrosstab td.number{ text-align: right; }
#MyCrosstab td.rowhead{ background-color: #C5DC9C; font-weight: bold;}
#MyCrosstab th{ background-color: #C5DC9C; font-size: small;  border-bottom: text-align: center; }
#MyCrosstab .sum{ border-top: 3px double #828282; }
#MyCrosstab .sumrow{ border-top: 1px solid #828282; text-align: right }
#MyCrosstab .total{ border-left: 1px solid #828282; }
-->
/*]]>*/
</style>' 

... will give you this

No. Sales per year

  1996 1997 1998 Total
Margaret Peacock 31 81 44 156
Janet Leverling 18 71 38 127
Nancy Davolio 26 55 42 123
Laura Callahan 19 54 31 104
Andrew Fuller 16 41 39 96
Robert King 11 36 25 72
Michael Suyama 15 33 19 67
Anne Dodsworth 5 19 19 43
Steven Buchanan 11 18 13 42
Sum 152 408 270 830

Lastly, although there is a lot more one can say about these procedures and the tricks one can use, especially with the CSS, here is an illustration of the way one might save the results of your crosstab to an HTML file for your management reporting intranet site. This uses Phil's technique taken from his blog entry Using BCP to export the contents of MAX datatypes to a file (Phil: Thanks for the plug, Robyn!)


DECLARE @HTMLString VARCHAR(MAX
EXECUTE spDynamicHTMLCrossTab
    
@colValue='DATENAME(year,orderDate)',
    
@rowValue='DATENAME(month,orderDate)',
    
@Aggregate'sum(subtotal)',
   
@Rowsort='DATEpart(month,orderDate)',
    
@FromExpression='FROM Orders 
   INNER JOIN "Order Subtotals" 
       ON Orders.OrderID = "Order Subtotals".OrderID
   inner join employees on employees.EmployeeID =orders.EmployeeID'
,
    
@ColOrderValue='datepart(year,orderDate)',
   
@Title ='Customers orders per month ',
   
@sortby='max(sort) asc',
   
@Output=@HTMLString output
SELECT @HTMLString=
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
                           "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head><title>Customers orders per month</title></head>
<body>'
+@HTMLString+'</body>'
EXECUTE spSaveTextToFile @HTMLString'C:\MyHTMLReport.html'

So that's it. We've enjoyed ourselves trying things out, and we've been surprised how far we can take the dynamic creation of pivot tables. We suggest you take the workshop and explore the ideas. If you discover anything interesting, we'd love to hear your comments! We'd particularly like to hear of interesting CSS layouts, though displaying them on Simple-Talk will be very difficult.



This article has been viewed 14223 times.
Robyn Page and Phil Factor

Author profile: Robyn Page and Phil Factor

Robyn Page is a consultant with Enformatica and USP Networks. She is also a well known actress, being most famous for her role as Katie Williams, barmaid in the Television Series Family Affairs.

Phil Factor (real name withheld to protect the guilty), aka Database Mole, has 20 years of experience with database-intensive applications. Despite having once been shouted at by a furious Bill Gates at an exhibition in the early 1980s, he has remained resolutely anonymous throughout his career.

Search for other articles by Robyn Page and Phil Factor

Rate this article:   Avg rating: from a total of 45 votes.


Poor

OK

Good

Great

Must read
 
Have Your Say
Do you have an opinion on this article? Then add your comment below:


Subject: CrossTabs & Pivots In SQL 2005
Posted by: Fahim (not signed in)
Posted on: Tuesday, July 24, 2007 at 4:21 PM
Message: How does one go about
constructing a dynamically generated crosstab resultset using the Pivot function in SQL 2005.!?

I have data that spans various years, but the range is flexible depending on the object on whom the data is currently being aggregated.

I.E. Product sales over the lifetime of availability, ranging over the various product types. I don't want to write a separate function for each product type (since this is the dynamic content), rather just generate the year-sales column on the fly.

RSVP feemurk@gmail.com

Subject: CrossTabs & Pivots In SQL 2005 & SQL 2000
Posted by: Greg Obleshchuk (not signed in)
Posted on: Tuesday, July 24, 2007 at 10:25 PM
Message: While this is a good article a lot of time an effort goes into creating a dynamic pivot SP. I don't know why people bother. GeckoWare Australia (my company) has been selling(online) our Crosstab/Pivot Extended Stored Procedure for SQL 2000 and SQL 2005 for the past 5 years. This si a great tool and one that can turn the nightmare of created pivots into a 2 minute process. Our tool even support multiple value and multi crosstab columns all for US$150 per server instance. It can be downloaded at http://www.geckoware.com.au

Great article but a lot of time wasted.

Kind regards
Greg Obleshchuk
Director
GeckoWare Australia

Subject: Great Article & GeckoWare is down-under
Posted by: JJ (not signed in)
Posted on: Wednesday, July 25, 2007 at 2:40 AM
Message: Great Article - Thanks
I wish the Pivot feature in 2005 was more Dynamic :-(

Here are some more great articles on dynamic cross-tabs:
http://www.sqlservercentral.com/columnists/plarsson/pivottableformicrosoftsqlserver.asp

http://www.sqlteam.com/article/dynamic-cross-tabs-pivot-tables

http://www.sqlmag.com/Articles/Index.cfm?ArticleID=15608

http://geekswithblogs.net/lorint/archive/2006/08/04/87166.aspx


As for Geckoware = Sorry Mate but I went to your site and tried to run the your online demo but it went to never never land and did not return

-jj

Subject: Re: GeckoWare & JJ
Posted by: Phil Factor (view profile)
Posted on: Wednesday, July 25, 2007 at 2:53 AM
Message: Thanks for the compliment. We really appreciate that.

Perhaps we didn't emphasise the fact well enough but Greg of Geckoware may not have spotted that we were aiming to go beyond doing a pivot, in order produce HTML presentation-quality reports of the results. I may have missed something, but I don't think that Geckoware does this!

Other than the bitmaps of the SMSS results grid, The various illustrations in the article are done by pasting the HTML output from the code directly into the article. No trickery or bitmaps. This is the actual output.

Apologies to JJ. Robyn says in the article 'Why bother with this when the Internet abounds with such Cross tab or pivot-table procedures?'. We needed to do this so that you end up with two procedures, one that produces a plain crosstab that produces a resultset, and another one with the same parameters for doing the result as an XHTML fragment, for pasting into reports or HTML files.

With the two procedures, you can use the first to develop reports for the second. It saves quite a bit of time.

Subject: HTML or XML
Posted by: Trevor Scurr (not signed in)
Posted on: Wednesday, July 25, 2007 at 5:13 AM
Message: Appreciate the article, but I'm not personally sure about the maintainability of a stored procedure containing embedded HTML tags. Has anyone put any thought into a pivot table that also uses SQLXML to generate an XML data set that can then be styled into HTML using xslt? I know you could argue that this is XHTML and hence XML but there is still a world of difference between embedding tags and letting SQLXML do the leg work. I'm about to go on vacation otherwise I'd have a play here myself - maybe when I come back!

Regards
Trevor


Subject: HTML or XML
Posted by: Trevor Scurr (not signed in)
Posted on: Wednesday, July 25, 2007 at 5:16 AM
Message: Hmmm just had a crashing realisation - of course SQLXML doesnt support grouping constructs which is the raison d'etre of pivot tables. Guess some further thinking is required....

Subject: HTML or XML
Posted by: Anonymous (not signed in)
Posted on: Wednesday, July 25, 2007 at 5:41 AM
Message: Sorry - me again.. you can do this with XML EXPLICIT. Taking the first query and changing to
SELECT  AS Tag, NULL AS Parent, NULL 

                   
AS [SalesXML!1!Sales!Element],
        NULL       
AS [Sales!2!SalesPerYear!Element],
        NULL       
AS [Sales!2!Year1996!Element], NULL 
                   
AS [Sales!2!Year1997!Element],
        NULL       
AS [Sales!2!Year1998!Element]
        NULL       
AS [Sales!2!Total!Element]
UNION
SELECT  
AS TagAS Parent,    
       NULL 
AS [SalesXML!1!Sales!Element],
        
[Sales!2!SalesPerYear!Element] 
           
CASE WHEN row IS NULL THEN 'Sum'
                  
ELSE CONVERT(VARCHAR(80), [row])
            
END,
        
[Sales!2!Year1996!Element] 
           
SUM(CASE col
               
WHEN '1996' THEN data
               
ELSE 0
           
END),
        
[Sales!2!Year1997!Element] 
           
SUM(CASE col
               
WHEN '1997' THEN data
               
ELSE 0
           
END),
        
[Sales!2!Year1998!Element] 
           
SUM(CASE col
              
WHEN '1998' THEN data
              
ELSE 0
            
END),
        
[Sales!2!Total!Element] SUM(data)
FROM    SELECT  [row] firstname ' ' lastname
                  
[col] YEAR(OrderDate),
                  
[data] COUNT(*)
          
FROM    Employees INNER JOIN Orders 
              
ON Employees.EmployeeID Orders.EmployeeID )
          
GROUP BY firstname ' ' lastnameYEAR(OrderDate)
        ) 
f
GROUP BY row
        
WITH ROLLUP
ORDER BY ParentTag[Sales!2!Total!Element] DESC 
FOR 
XML EXPLICIT

 

 

 

gives a reasonable XML set although I probably would go on to play with the row ordering. For the future I think I will look at the generalised pivot query


Subject: Re: HTML or XML
Posted by: Robyn Page (view profile)
Posted on: Wednesday, July 25, 2007 at 6:52 AM
Message: We initially decided that we'd go the HTML/XHTML route purely because we wanted something that the SQL Server 2000 people could join in with.

Subsequently Phil decided it would round off the article quite nicely to show off how simple it was to use FOR XML to produce XHTML fragments containing the pivot table. He's still trying, so how about a Simple-Talk goodie-bag for the best solution? (Phil loves the cute Red-Gate Memory stick which is included)

Subject: Great Article
Posted by: Ben (view profile)
Posted on: Wednesday, July 25, 2007 at 7:58 AM
Message: This is a wonderful topic. Pivot tables are one of the best data mining capabilities in SQL.

Any time someone gets away from a series of hard coded CASE statements you really extend the functionality of your code. I have written a number of articles on the same theme using pivot queries and a table with date ranges to generate a rolling window of time. You may enjoy looking at some of those at www.sswug.org.

Cheers,

Ben Taylor

Subject: Re: HTML or XML
Posted by: Trevor Scurr (not signed in)
Posted on: Wednesday, July 25, 2007 at 8:47 AM
Message: Just for the record the above query works on SQL Server 2000 with the SQL XML 3.0 library. In common with many of your readers I'm sure we still find our clients primarily using 2000. And SQL Server 2008 is looming....!

Subject: Revealing My Ignorance...
Posted by: Noel Cruz (not signed in)
Posted on: Wednesday, July 25, 2007 at 9:32 AM
Message: In an attempt to implement (create) the proc that produces the HTML output, I get an error on the following piece of code:

INSERT INTO @StringTable(string,waste)
EXECUTE (@DataRows)

The error states, "Msg 197, Level 15, State 1, Procedure spDynamicHTMLCrossTab, Line 164
EXECUTE cannot be used as a source when inserting into a table variable."

Any suggestions?

Subject: This is what Analysis Services is for
Posted by: DuWayne Willett (not signed in)
Posted on: Wednesday, July 25, 2007 at 9:52 AM
Message: Well, you can do extended calisthenics to do this type of PivotTable reporting using SQL, but SQL is fundamentally not the optimal tool for the job.

Dimensionally-modeled databases/cubes do this type of reporting very easily.
A cleaner and simpler way of doing all the above is to use Analysis Services (or another OLAP engine) to construct a cube, with queries written in MDX (multidimensional expressions), not SQL.
Fortunately the end-user tools (including Excel PivotTables, or HTML equivalents), produce the MDX themselves automatically when the user drags-and-drops selections in the PivotTable. You can write your own MDX queries if you want to, but it's not required.

Subject: a few additional wrinkles
Posted by: dterrie (view profile)
Posted on: Wednesday, July 25, 2007 at 10:09 AM
Message: At a previous job, we had a .net app that generated HTML pages, complete with tables similarly formatted using tablestyle css. We had a few additional features: colspans in header rows, and oddrow and evenrow background color options. A sproc unioned these rows into it's output along with a Format column, not used in the table, that was populated with a row type case statement. We then iterated through the rows and columns to set the styles and content.

Subject: Re Revealing my ignorance
Posted by: Phil Factor (view profile)
Posted on: Wednesday, July 25, 2007 at 10:24 AM
Message: I've just popped a SQL Server 2000 version of the second stored procedure in the 'speechbox' at the top of the article with the other two files. In SQL Server 2000 you have to use a temporary table intead of a table variable. You also can't use Varchar(MAX) and there is a bug in the GROUP BY which gives an error with perfectly correct syntax!

Subject: Thanks!
Posted by: Noel Cruz (not signed in)
Posted on: Wednesday, July 25, 2007 at 11:43 AM
Message: Thanks a lot, Phil!

Before seeing this, I did change the VARCHAR(MAX) appropriately for SQL Server 2000, and I was attempting to fix the GROUP BY stuff as well.

Anyway, thanks for the help!

Subject: Edits for SQL Server 2000
Posted by: Mark Hill (not signed in)
Posted on: Wednesday, July 25, 2007 at 1:22 PM
Message: Great article, but I have SQL Server 2000, and your code includes some SQL Server 2005 idioms that I had to change. In addition to the minor VARCHAR(8000) for VARCHAR(MAX) replacement that you mentioned, I also needed to replace NVARCHAR(MAX) with NVARCHAR(4000). But the most serious change is to circumvent SQL Server 2000's inability to 'GROUP BY '+@ColValue when the @ColValue is used in an expression. (It complains that "Column '...' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.") I accomplished this by putting the grouping in a subquery. In spDynamicCrossTab, I changed then "SELECT @Command=" statement to:

SELECT @Command='select @SQL=coalesce(@SQL,''SELECT
['+@Title+']=case when row is null then ''''Sum'''' else convert(Varchar(80),[row]) end ,
'')+
''[''+convert(varchar(100),
_colvalue_)+''] =sum( CASE col WHEN ''''''+convert(varchar(100),
_colvalue_)+'''''' THEN data else 0 END ),
'' FROM (SELECT TOP 100 PERCENT '+@ColValue+' AS _colvalue_ '+@FromExpression+'
GROUP BY '+@ColValue+'
order by max('+@ColorderValue+')) subq'

Subject: more flexibility possible
Posted by: jenniebee (view profile)
Posted on: Wednesday, July 25, 2007 at 2:32 PM
Message: This uses XML to do the pivoting, no dynamic sql or case statements needed:

;WITH  Last3Years OrderYear )
        
AS SELECT DISTINCT TOP 3
                    
CONVERT(CHAR(4), OrderDate102)
             
FROM   Orders o
             
WHERE  [EmployeeID] IS NOT NULL
             
ORDER BY CONVERT(CHAR(4), OrderDate102DESC
           
) ,
      
OrdersByYear EmployeeIDEmployeeNameOrderYearTotalOrders )
        
AS SELECT e.EmployeeID,
                    
e.[FirstName] ' ' e.[LastName] AS EmployeeName,
                    
l3y.OrderYearSUM(os.[Subtotal]AS TotalOrders
             
FROM   Orders o 
             
JOIN [Employees] e ON o.[EmployeeID] e.[EmployeeID] 
             
JOIN [Order Subtotals] os ON o.[OrderID] os.[OrderID] 
             
JOIN Last3Years l3y ON l3y.OrderYear YEAR(o.OrderDate)
             
GROUP BY e.[EmployeeID]e.[FirstName] ' ' e.[LastName],
                    
l3y.OrderYear
           
)
  
SELECT  Tag 1Parent NULL, AS [table!1!!HIDE],
          
CAST(NULL AS VARCHAR(12)) AS [tr!2!empid!HIDE],
          NULL 
AS [th!3!colorder!HIDE],
          
CAST(NULL AS VARCHAR(15)) AS [th!3!!ELEMENT],
          
CAST(NULL AS VARCHAR(4)) AS [td!4!colorder!HIDE],
          
CAST(NULL AS VARCHAR(12)) AS [td!4!class],
          
CAST(NULL AS VARCHAR(12)) AS [td!4!!ELEMENT]
  
UNION ALL
  
SELECT  Tag 2Parent 12-- puts <table> before <tr>
          
hc-- puts <th> before <td>
          
NULL, NULL, -- header row
          
NULL, NULL, NULL -- data
  
FROM    SELECT  CAST(AS VARCHAR(