Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / SQL-Server / SQL-Server-2008

SQL Wizardry Part Seven - PIVOT and arbitrary lists of data

4.98/5 (13 votes)
16 Jan 2014CPOL12 min read 24.4K   208  
Discussion on pivot and other ways to turn columns of data in to rows in SQL Server

Introduction

In a previous article, I talked about passing an arbitrary number of values in to SQL Server. But, what if you want to get an arbitrary number of values OUT, represented in a clean data set ? That's what we're looking at today.

The data

If you don't have AdventureWorks installed, go and install it now. You can google for a current link, just make sure you install the one for your SQL Server version. I like using this database because it means you have a rich set of data to work against, without having to install much of anything.

A flat solution

We're going to look at the salesorderdetail table, and we're going to get the sum of prices by month and year. To get this out in normal SQL, looks like this:

SQL
select year(modifieddate) as year, month(modifieddate) as month, sum(UnitPrice) as unitprice 
  from sales.SalesOrderDetail 
  group by year (modifieddate), month(modifieddate) 
  order by year(modifieddate), month(modifieddate)

This is perfectly workable a lot of the time. However, we want a format where each row is one year, and the months go across the row, one column for each.

PIVOT

The first thing we're going to look at, is the PIVOT key word. This is used to take a set of data and flip it by 90 degrees, if you will, so that things that were individual rows, become individual columns in the data. This is really the main way of doing this, everything else is alternatives that are usually not as good.

Here is the pivot code:

SQL
select [year], isnull([1], 0) as 'Jan', isnull([2], 0) as 'Feb', isnull([3], 0) as 'Mar', isnull([4], 0) as 'Apr', isnull([5], 0) as 'May', isnull([6], 0) as 'Jun', isnull([7], 0) as 'Jul', isnull([8], 0) as 'Aug', isnull([9], 0) as 'Sep', isnull([10], 0) as 'Oct', isnull([11], 0) as 'Nov', isnull([12], 0) as 'Dec'
from
(
  select UnitPrice as unitprice, year(modifieddate) as year, month(modifieddate) as month from sales.SalesOrderDetail
) as src
pivot
(
sum(unitprice) for month in ([1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12])
) as pvt
order by [year];

First, run it. You'll see this is much nicer, especially for things like comparing the same month for different years, etc. The syntax can look confusing at first, so let's work through it.

The core of it is this statement:

SQL
select UnitPrice as unitprice, year(modifieddate) as year, month(modifieddate) as month from sales.SalesOrderDetail

which returns data like this:

unitprice	year	month
461.694	2008	6
600.2625	2006	9
36.83	2007	8
3399.99	2005	9
31.584	2007	9
32.9945	2006	9
3374.99	2006	5
21.98	2007	10
2024.994	2005	8
40.5942	2007	7
818.70	2006	2
....

but with duplicates ( because of the same item being ordered more than once in the month.

The main select looks like this:

SQL
select [year], isnull([1], 0) as 'Jan', isnull([2], 0) as 'Feb', isnull([3], 0) as 'Mar', isnull([4], 0) as 'Apr', isnull([5], 0) as 'May', isnull([6], 0) as 'Jun', isnull([7], 0) as 'Jul', isnull([8], 0) as 'Aug', isnull([9], 0) as 'Sep', isnull([10], 0) as 'Oct', isnull([11], 0) as 'Nov', isnull([12], 0) as 'Dec'

The IsNull statements just turn null values in to 0s, to make it more readable. You can remove them if you want to show 'I had no data' as being separate to 'the data was 0'. We are then reading columns named from 1 to 12, and then renaming them to the name of the month in question. But, we don't select those values at all. They come from the pivot statement, which is this:

SQL
pivot
(
sum(unitprice) for month in ([1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12])
) as pvt

So what the pivot does, is take the data from the core statement, and choose what to pivot on. In this case, it's going to pivot on the month value, and create columns with values from 1-12. Note - you HAVE to know your full list of end values, you CANNOT do this:

SQL
pivot
(
sum(unitprice) for month in (select id from months)
) as pvt

More on this later, but this is the main weakness with all the approaches I am going to show you before the last one.

So, the year value is not listed as a pivot value, or as an aggregate value, so it is just passed through. The month becomes the window for the sum operation, in that sense, it's doing the same things as our initial statement. What is nice is, the year IS being considered, if it was not, the values for all our rows would be the same. So, any rows you add, create the window, not just the one you pivot on.

So, a pivot involves the following:

  • a core statement to get the data you want
  • a pivot statement that defines the value to return against the pivot, the value to pivot on, and the range of possible values
  • a select statement that selects the combination of data from the core statement, and the pivoted values that you want.

It's really not that hard, but it takes some getting used to. I recommend playing with it for a while, coming up with some ideas for ways to pivot data, and then working out how to do them. One suggestion - what if you wanted to pivot this example, but with quarterly data ?

Using Case

You can reproduce what a pivot does by writing your own case statements to create the columns and get them to aggregate accordingly. It's pretty straightforward, here's the example:

SQL
SELECT YEAR(modifieddate) AS OrderYear,
       SUM(CASE WHEN DATEPART(m, modifieddate) = 1
                THEN unitPrice ELSE 0 END) AS 'Jan',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 2
                THEN unitPrice ELSE 0 END) AS 'Feb',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 3
                THEN unitPrice ELSE 0 END) AS 'Mar',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 4
                THEN unitPrice ELSE 0 END) AS 'Apr',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 5
                THEN unitPrice ELSE 0 END) AS 'May',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 6
                THEN unitPrice ELSE 0 END) AS 'Jun',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 7
                THEN unitPrice ELSE 0 END) AS 'Jul',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 8
                THEN unitPrice ELSE 0 END) AS 'Aug',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 9
                THEN unitPrice ELSE 0 END) AS 'Sep',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 10
                THEN unitPrice ELSE 0 END) AS 'Oct',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 11
                THEN unitPrice ELSE 0 END) AS 'Nov',
       SUM(CASE WHEN DATEPART(m, modifieddate) = 12
                THEN unitPrice ELSE 0 END) AS 'Dec'
FROM sales.SalesOrderDetail
GROUP BY DATEPART(yyyy, modifieddate)
order by orderyear;

So we use a sum statement for each column, and inside that we put a case statement to only aggregate values for that month, and we use a group by to define the window for each sum statement. Note that because of the order of operations in a SQL query, the column does not exist yet when our GROUP BY occurs, so I cannot GROUP BY ORDERYEAR, I have to use the datepart statement again. Note also that year(modifieddate) and datepart(yyyy, modifieddate) are the same thing.

The David Rozenshtein method

This is a method I stumbled across while searching to make sure this article was as complete as possible. David appears to be an author of Transact SQL books, so I assume this method appeared in one of his books. It essentially uses some trickery to do the same as the case statement, but avoid the case statement. Here it is:

SQL
SELECT YEAR(modifieddate) AS OrderYear,
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 1)))) AS 'Jan',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 2)))) AS 'Feb',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 3)))) AS 'Mar',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 4)))) AS 'Apr',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 5)))) AS 'May',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 6)))) AS 'Jun',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 7)))) AS 'Jul',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 8)))) AS 'Aug',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 9)))) AS 'Sep',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 10)))) AS 'Oct',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 11)))) AS 'Nov',
       SUM(unitPrice * (1 - ABS(SIGN(MONTH(modifieddate) - 12)))) AS 'Dec'
FROM sales.SalesOrderDetail
GROUP BY YEAR(modifieddate)
order by orderyear;

Basically, it uses some trickery around the return date of the MONTH function ( note, the original uses DATEPART, but I thought it was hard enough to read as it was ). The end result is that for Jan, the unitprice is multiplied by 1 if the month is Jan, and by 0 otherwise.

I've put some code in to the SQL file for you to run, like this:

SQL
select (1 - ABS(SIGN(2 - 1)))
select (1 - ABS(SIGN(2 - 2)))
select (1 - ABS(SIGN(2 - 3)))

This will return 0, 1, 0. What is it doing ? Sign will return -1 for values < 0, 0 for 0, and 1 for values > 0. So, sign( -12 ) will return -1. sign (12) will return 1. ABS returns the absolute value. This means that the negative sign is stripped. So, abs(sign(xxx)) will return 0 if xxx is zero, otherwise it returns 1. So, unitPrice * (1 - abs(sign(xxx)) means unitPrice * 1 if xxx is 0, and unitPrice * 1 if xxx is not 0. That's basically it. It's a bit 'clever' for my tastes, I'm not sure ( and I admit I've done no testing ) if SQL Server is faster at doing this sort of RBAR ( row by row ) math, compared to row by row case statements.

Using a matrix table

The idea here is simple. Create a table that creates a lookup table to give you the 1s and 0s that the previous solution was calculating, with the same end effect. Here is the SQL to create the table:

SQL
CREATE TABLE MonthMatrix (
  month_nbr INT NOT NULL PRIMARY KEY
            CHECK (month_nbr BETWEEN 1 AND 12),
  jan INT NOT NULL DEFAULT 0
      CHECK (jan IN (0, 1)),
  feb INT NOT NULL DEFAULT 0
      CHECK (feb IN (0, 1)),
  mar INT NOT NULL DEFAULT 0
      CHECK (mar IN (0, 1)),
  apr INT NOT NULL DEFAULT 0
      CHECK (apr IN (0, 1)),
  may INT NOT NULL DEFAULT 0
      CHECK (may IN (0, 1)),
  jun INT NOT NULL DEFAULT 0
      CHECK (jun IN (0, 1)),
  jul INT NOT NULL DEFAULT 0
      CHECK (jul IN (0, 1)),
  aug INT NOT NULL DEFAULT 0
      CHECK (aug IN (0, 1)),
  sep INT NOT NULL DEFAULT 0
      CHECK (sep IN (0, 1)),
  oct INT NOT NULL DEFAULT 0
      CHECK (oct IN (0, 1)),
  nov INT NOT NULL DEFAULT 0
      CHECK (nov IN (0, 1)),
  dec INT NOT NULL DEFAULT 0
      CHECK (dec IN (0, 1)));
 
-- Populate the matrix table
INSERT INTO MonthMatrix (month_nbr, jan) VALUES (1, 1);
INSERT INTO MonthMatrix (month_nbr, feb) VALUES (2, 1);
INSERT INTO MonthMatrix (month_nbr, mar) VALUES (3, 1);
INSERT INTO MonthMatrix (month_nbr, apr) VALUES (4, 1);
INSERT INTO MonthMatrix (month_nbr, may) VALUES (5, 1);
INSERT INTO MonthMatrix (month_nbr, jun) VALUES (6, 1);
INSERT INTO MonthMatrix (month_nbr, jul) VALUES (7, 1);
INSERT INTO MonthMatrix (month_nbr, aug) VALUES (8, 1);
INSERT INTO MonthMatrix (month_nbr, sep) VALUES (9, 1);
INSERT INTO MonthMatrix (month_nbr, oct) VALUES (10, 1);
INSERT INTO MonthMatrix (month_nbr, nov) VALUES (11, 1);
INSERT INTO MonthMatrix (month_nbr, dec) VALUES (12, 1);

And once you have that, you can do this:

SQL
SELECT DATEPART(yyyy, modifieddate) AS OrderYear,
       SUM(UnitPrice * jan) AS 'Jan',
       SUM(UnitPrice * feb) AS 'Feb',
       SUM(UnitPrice * mar) AS 'Mar',
       SUM(UnitPrice * apr) AS 'Apr',
       SUM(UnitPrice * may) AS 'May',
       SUM(UnitPrice * jun) AS 'Jun',
       SUM(UnitPrice * jul) AS 'Jul',
       SUM(UnitPrice * Aug) AS 'Aug',
       SUM(UnitPrice * Sep) AS 'Sep',
       SUM(UnitPrice * Oct) AS 'Oct',
       SUM(UnitPrice * Nov) AS 'Nov',
       SUM(UnitPrice * Dec) AS 'Dec'
FROM Sales.SalesOrderDetail AS S
JOIN MonthMatrix AS M
  ON DATEPART(m, S.modifieddate) = M.month_nbr
GROUP BY DATEPART(yyyy, modifieddate)
order by orderyear;

Hopefully the way it works is obvious. It's got to be quicker than both of the prior solutions, as it's fully set based.

I can see using it for months and quarters, but I can't see creating a matrix table for each possible set of values I want to PIVOT.

Using outer apply

The cross apply operator calls every row in table1 for every row in table2. As such, it's cost is exponential. This also means the cost of adding columns to pivot on, is exponential. I don't recommend EVER doing this, the samples I found online pivoted on 3 values. This SQL, which pivots on 12, ran for 16 hours before I gave up and stopped it.

SQL
SELECT distinct DATEPART(yyyy, modifieddate), D1.[Jan], D2.[Feb], D3.[Mar], D4.[Apr], D5.[May], D6.[Jun], D7.[Jul], 

D8.Aug, D9.Sep, D10.Oct, D11.Nov, D12.[Dec], T.Total
FROM Sales.SalesOrderDetail SOD
OUTER APPLY (SELECT DATEPART(yyyy, modifieddate) as [year], SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Jan] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 1) D1
OUTER APPLY (SELECT DATEPART(yyyy, modifieddate) as [year], SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Feb] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 2) D2
OUTER APPLY (SELECT DATEPART(yyyy, modifieddate) as [year], SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Mar] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 3) D3
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Apr] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 4) D4
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [May] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 5) D5
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Jun] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 6) D6
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Jul] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 7) D7
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Aug] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 8) D8
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Sep] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 9) D9
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Oct] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 10) D10
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Nov] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 11) D11
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS [Dec] 
from  Sales.SalesOrderDetail s1
where month(modifieddate) = 12) D12
OUTER APPLY (SELECT SUM(UnitPrice) over (partition by DATEPART(yyyy, modifieddate)) AS Total
FROM  Sales.SalesOrderDetail S
WHERE year(S.modifieddate) = year(sod.ModifiedDate) 
) t

The story so far

So we've looked at several possible ways to do the same thing, with all the other ways really being ways to avoid the pivot keyword, in my opinion for no good reason unless you're stuck with a pre SS2005 database. However, the answer to the question 'what is the best way' is often 'it depends', so I've tried to find a number of alternatives and present them, as I am sure there are times when each of these ( even outer apply ) is the best choice. However, they all suffer from the same basic issue. You need to know ahead of time what your pivot values are. This is fine for time based queries ( there will never be a fifth quarter or a 13th month or a 32nd day of the month ). However, it is a serious liability for other forms of data, such as user names. The way I see this 'solved' in online articles is procs that build a SQL string using a select, then use EXEC to execute that string mashed SQL. I would NEVER do this. I don't see any point in using stored procs, if all they do, is string mash and execute semi arbitrary SQL. But, I admit, if you want one column per value, that is probably the only way to do it, with an arbitrary list of values.

Using FOR XML

However, if you can live with getting your values all scrunched up in one column, then there is a solution. I've seen this solution widely presented on the web in a 'single shot copy and paste' sort of way. I want to walk you through it, so you can understand what it is doing. So, please don't go to the end, bare with me, and run each statement I am giving you in turn, so you can understand what they are doing.

First, please run this:

SQL
SELECT distinct month(modifieddate)
            FROM sales.SalesOrderDetail  s
            FOR XML PATH('month')

You get:

XML
XML_F52E2B61-18A1-11d1-B105-00805F49916B
<month>9</month><month>3</month><month>12</month><month>6</month><month>7</month><month>1</month><month>10</month><month>4</month><month>5</month><month>2</month><month>11</month><month>8</month>

Note that your column has a random name and the month ids, in XML nodes called 'month'. This is an XML snippet, it's not valid XML as it has no root

Now run this:

SQL
SELECT distinct convert(varchar(2), month(modifieddate))
            FROM sales.SalesOrderDetail  s
            FOR XML PATH('')

You get back something like this:

SQL
XML_F52E2B61-18A1-11d1-B105-00805F49916B
312982611171054

Interestingly, the order changes when you remove the node name, but what we see here is, you can ask for XML without specifying a node name, and you just get back the string with no XML nodes.

Now try this:

SQL
SELECT distinct ', ' +convert(varchar(2), month(modifieddate))
            FROM sales.SalesOrderDetail  s
            FOR XML PATH('')

This returns:

SQL
XML_F52E2B61-18A1-11d1-B105-00805F49916B
, 11, 1, 8, 3, 6, 9, 4, 2, 10, 5, 12, 7

which is not surprising, but clearly we can now see how this is starting to be useful to us.

Now let's try something different. Run this:

SQL
select stuff(', 1, 2, 3, 4, 5, 6', 1, 2, '')

The stuff method allows you to specify the start and length of a string to remove, and the string to replace it with.

This seems a little arcane to me, but it suits our purposes. Taking a string in the format we're able to generate, and taking a 2 character string, from the first ordinal, and replacing it with an empty string, gives us this:

SQL
1, 2, 3, 4, 5, 6

So if we put these two ideas together, we can run this:

SQL
SELECT
distinct year(modifieddate),
    STUFF
    (
        (
            SELECT ',' + convert(varchar(2), month(modifieddate))
			FROM sales.SalesOrderDetail  s
            WHERE year(s.modifieddate) = year(so.ModifiedDate)
            group BY month(modifieddate)
			order by month(modifieddate)
            FOR XML PATH('')
        ), 1, 1, ''
    ) AS Months
FROM
    sales.SalesOrderDetail so order by year(modifieddate)

This gives us a list of months for each year. These are the months for which we have values. Now let's make it useful:

SQL
SELECT distinct year(modifieddate),
    STUFF
    (
        (
            SELECT ',' + convert(varchar(2), month(modifieddate)) + ': ' + convert(varchar(10), sum(UnitPrice))
            FROM sales.SalesOrderDetail  s
            WHERE year(s.modifieddate) = year(so.ModifiedDate)
            group BY month(modifieddate)
			order by month(modifieddate)
            FOR XML PATH('')
        ), 1, 1, ''
    ) AS Months
FROM
    sales.SalesOrderDetail so order by year(modifieddate)

This is a LOT slower than doing a PIVOT. The result looks like this:

SQL
Year	Months
2005	7: 704154.94,8: 1101956.11,9: 913860.60,10: 851378.61,11: 1376044.03,12: 1345397.82
2006	1: 901717.40,2: 1327442.35,3: 1208028.43,4: 1025423.92,5: 1520124.39,6: 1129626.03,7: 1104691.22,8: 1650825.53,9: 1148641.00,10: 952477.60,11: 1302360.94,12: 1282373.57
2007	1: 860448.52,2: 1302379.28,3: 1032358.48,4: 1042209.28,5: 1486359.64,6: 1217570.78,7: 1653869.56,8: 2135760.74,9: 2302144.85,10: 1767180.26,11: 2348285.76,12: 2981398.52
2008	1: 1942234.14,2: 2459396.65,3: 2537894.73,4: 2291712.32,5: 2994454.75,6: 3170724.26,7: 50840.63

Note I had to convert my values in to varchar in order to be able to concatenate them, but I was able to use things like sum in there, and generate exactly the data I wanted. I deliberately did not put a space after the comma, to show that I can change the parameters to STUFF for any length leading string to remove.

In this case, I think PIVOT is both faster, and more convenient. So let's look at a better example. I want, for each order, to get a list of all the products that were in that order. Hopefully having walked through the previous example, you can look at this one and see what it is doing.

SQL
select s.SalesOrderId, 
	stuff
	(
		(
		 SELECT ';' + p.Name
            from Production.Product p
			inner join sales.SalesOrderDetail so on so.ProductId = p.ProductId
			where s.salesorderid = so.SalesOrderID
            FOR XML PATH('')
        ), 1, 1, ''
	) as Products
from 
  sales.SalesOrderDetail s group by salesorderid
  order by salesorderId

As the product names contain commas, I've used a semicolon as a delimiter. Here is a sample of the output:

SalesOrderIdProducts
43659Mountain-100 Black, 42;Mountain-100 Black, 44;Mountain-100 Black, 48;Mountain-100 Silver, 38;Mountain-100 Silver, 42;Mountain-100 Silver, 44;Mountain-100 Silver, 48;Long-Sleeve Logo Jersey, M;Long-Sleeve Logo Jersey, XL;Mountain Bike Socks, M;AWC Logo Cap;Sport-100 Helmet, Blue
43660Road-650 Red, 44;Road-450 Red, 52
43661HL Mountain Frame - Black, 48;HL Mountain Frame - Black, 42;HL Mountain Frame - Black, 38;AWC Logo Cap;Long-Sleeve Logo Jersey, L;HL Mountain Frame - Silver, 46;Mountain-100 Black, 38;Mountain-100 Black, 48;Sport-100 Helmet, Blue;HL Mountain Frame - Silver, 48;Mountain-100 Black, 42;Mountain-100 Silver, 44;Long-Sleeve Logo Jersey, XL;Mountain-100 Black, 44;Sport-100 Helmet, Black
43662Road-650 Red, 52;Road-650 Black, 52;LL Road Frame - Red, 62;Road-450 Red, 58;LL Road Frame - Red, 44;Road-650 Red, 44;Road-650 Black, 58;Road-650 Black, 44;Road-150 Red, 56;Road-450 Red, 44;Road-650 Red, 48;ML Road Frame - Red, 48;Road-450 Red, 52;LL Road Frame - Red, 60;LL Road Frame - Black, 58;Road-150 Red, 62;Road-650 Red, 60;LL Road Frame - Red, 48;ML Road Frame - Red, 52;LL Road Frame - Black, 52;Road-650 Black, 60;Road-450 Red, 60
43663Road-650 Red, 60
43664Mountain-100 Silver, 42;Mountain-100 Black, 38;Long-Sleeve Logo Jersey, M;Long-Sleeve Logo Jersey, XL;Mountain-100 Black, 44;Mountain-100 Silver, 38;Mountain-100 Silver, 44;Mountain-100 Black, 48
43665Sport-100 Helmet, Blue;Mountain-100 Silver, 44;Sport-100 Helmet, Red;Long-Sleeve Logo Jersey, L;Mountain-100 Black, 44;AWC Logo Cap;Mountain-100 Black, 38;Mountain-100 Black, 48;Mountain Bike Socks, M;Mountain-100 Black, 42
43666Road-650 Red, 52;Road-150 Red, 56;ML Road Frame - Red, 48;Road-450 Red, 44;Road-650 Black, 44;Road- 650 Black, 60
43667Mountain Bike Socks, L;Mountain-100 Silver, 44;Mountain-100 Black, 48;Mountain-100 Black, 38
43668Road-450 Red, 44;Road-150 Red, 56;Road-650 Red, 60;Road-650 Black, 58;Long-Sleeve Logo Jersey, L;LL Road Frame - Red, 62;Sport-100 Helmet, Red;Sport-100 Helmet, Blue;Road-450 Red, 58;AWC Logo Cap;LL Road Frame - Red, 60;Road-450 Red, 60;Road-650 Red, 62;Road-650 Black, 52;LL Road Frame - Red, 48;Road-650 Red, 52;Road-650 Black, 60;LL Road Frame - Red, 44;Long-Sleeve Logo Jersey, XL;Road-650 Black, 44;ML Road Frame - Red, 48;Road-450 Red, 52;Road-650 Red, 44;LL Road Frame - Black, 52;Long-Sleeve Logo Jersey, M;LL Road Frame - Black, 58;Sport-100 Helmet, Black;ML Road Frame - Red, 52;Road-650 Red, 48
43669HL Mountain Frame - Black, 38
43670Mountain Bike Socks, L;Mountain Bike Socks, M;Mountain-100 Silver, 44;Mountain-100 Black, 42
43671Road-150 Red, 56;Long-Sleeve Logo Jersey, M;Road-450 Red, 44;Road-650 Black, 44;ML Road Frame - Red, 48;Road-650 Red, 48;Road-450 Red, 60;Road-650 Red, 52;Long-Sleeve Logo Jersey, XL;Sport-100 Helmet, Blue;Sport-100 Helmet, Black
43672Mountain Bike Socks, M;Mountain-100 Black, 42;Mountain-100 Silver, 48
43673Road-450 Red, 58;Long-Sleeve Logo Jersey, L;LL Road Frame - Red, 60;Road-650 Red, 62;Road-650 Red, 44;LL Road Frame - Black, 52;Road-650 Black, 52;AWC Logo Cap;Sport-100 Helmet, Black;Sport-100 Helmet, Blue;Sport-100 Helmet, Red
43674Road-450 Red, 52

Conclusion

Hopefully you've found this article helpful. I know that I was under the impression that pivot was hard to use, and six months ago, I had no idea how easy it is to get an arbitrary list of values back from SQL Server. It would be nice if pivot could use a SELECT to get a list of values to pivot on, even if it internally didn't do much better than the solutions I see online with string mashed SQL. However, the XML based solution is very flexible and very powerful, if you don't mind having all your data in the one column.

As always, I encourage any questions about SQL, and especially about the things discussed in this article. If you know something I missed, speak up so I can improve the article. I'll be on the forums looking for SQL questions ( although most seem to get answered while I sleep ), and working on another installment for next week.....

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)