<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://blogs.msdn.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Craig Freedman's SQL Server Blog : Pivot and Unpivot</title><link>http://blogs.msdn.com/craigfr/archive/tags/Pivot+and+Unpivot/default.aspx</link><description>Tags: Pivot and Unpivot</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP1 (Build: 61025.2)</generator><item><title>The UNPIVOT Operator</title><link>http://blogs.msdn.com/craigfr/archive/2007/07/17/the-unpivot-operator.aspx</link><pubDate>Tue, 17 Jul 2007 20:36:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:3920521</guid><dc:creator>craigfr</dc:creator><slash:comments>2</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/3920521.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=3920521</wfw:commentRss><description>&lt;P&gt;The UNPIVOT operator is the opposite of the PIVOT operator.&amp;nbsp; As I explained in &lt;A title="The PIVOT Operator" href="http://blogs.msdn.com/craigfr/archive/2007/07/03/the-pivot-operator.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2007/07/03/the-pivot-operator.aspx"&gt;my earlier post&lt;/A&gt;, the PIVOT operator takes a normalized table and transforms it into a new table where the columns of the new table are derived from the values in the original table.&amp;nbsp; The UNPIVOT operator takes a pivoted table and transforms it back into a normalized form with one row per data point using the column names as values in the result.&amp;nbsp; For example, suppose we have the following data:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE PIVOT_Sales(EmpId INT, [2005] MONEY, [2006] MONEY, [2007] MONEY)&lt;BR&gt;INSERT PIVOT_Sales VALUES(1, 12000, 18000, 25000)&lt;BR&gt;INSERT PIVOT_Sales VALUES(2, 15000, 6000, NULL)&lt;BR&gt;INSERT PIVOT_Sales VALUES(3, NULL, 20000, 24000)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;This is the output of the PIVOT operation from my earlier posts.&amp;nbsp; There is one row for each employee with up to three years of sales data per row.&amp;nbsp; If there is no sales data for a particular employee for a particular year, we simply insert NULL.&amp;nbsp; We can transform this table back to its original form with a single row per employee per year using the following UNPIVOT statement:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT EmpId, CAST (Yr AS INT) AS Yr, Sales&lt;BR&gt;FROM (SELECT EmpId, [2005], [2006], [2007] FROM PIVOT_Sales) AS p&lt;BR&gt;UNPIVOT (Sales FOR Yr IN ([2005], [2006], [2007])) AS s&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;I've explicitly casted the &lt;I&gt;Yr&lt;/I&gt; column to integer.&amp;nbsp; The default type for the &lt;I&gt;pivot column&lt;/I&gt; is NVARCHAR(128).&amp;nbsp; This type is based on the maximum column name length of 128 characters. &lt;/P&gt;
&lt;P&gt;The resulting output is:&lt;/P&gt;&lt;PRE&gt;EmpId       Yr          Sales
----------- ----------- ---------------------
1           2005        12000.00
1           2006        18000.00
1           2007        25000.00
2           2005        15000.00
2           2006        6000.00
3           2006        20000.00
3           2007        24000.00&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;Unlike PIVOT operations which may not be reversible, all UNPIVOT operations are reversible (so long as all of the input data is preserved).&amp;nbsp; That is, we can always transform the output of an UNPIVOT operation back into the original table using an appropriate PIVOT operation.&amp;nbsp; Unlike PIVOT operator, the UNPIVOT operator does not require or support aggregation functions.&lt;/P&gt;
&lt;P&gt;Let's look at the plan for the above query:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1010]=CONVERT(int,[Expr1009],0)))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Filter(WHERE:([Expr1008] IS NOT NULL))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Nested Loops(Left Outer Join, OUTER REFERENCES:([PIVOT_Sales].[2005], [PIVOT_Sales].[2006], [PIVOT_Sales].[2007]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:([PIVOT_Sales].[EmpId]=[PIVOT_Sales].[EmpId]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([PIVOT_Sales]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:((N'2005',[PIVOT_Sales].[2005]),(N'2006',[PIVOT_Sales].[2006]),(N'2007',[PIVOT_Sales].[2007])))&lt;/P&gt;
&lt;P mce_keep="true"&gt;This query plan simply takes each row of the input table and joins it with a constant scan that generates three rows - one for each of the three columns listed in the UNPIVOT IN clause - for each input row.&amp;nbsp; The plan then filters out any rows that have NULL data.&amp;nbsp; (Note that [Expr1008] is the &lt;I&gt;Sales&lt;/I&gt; column and [Expr1009] is the &lt;I&gt;Yr&lt;/I&gt; column.)&amp;nbsp; There are a couple of points worth noting about this query plan.&amp;nbsp; First, the join must be a nested loops join because the constant scan operator uses the correlated parameters from the outer side of the join to generate rows.&amp;nbsp; There is no way to generate these rows without these correlated parameters.&amp;nbsp; Second, the join need not be a left outer join.&amp;nbsp; The constant scan always produces exactly three rows and, thus, the outer rows always join and are never NULL extended.&amp;nbsp; Nevertheless, the outer join is harmless in this context and behaves like an inner join.&lt;/P&gt;
&lt;P&gt;Note that we can write the original query as:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT p.EmpId, Yr, Sales&lt;BR&gt;FROM PIVOT_Sales AS p CROSS APPLY&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT EmpId, 2005 AS Yr, [2005] AS Sales UNION ALL&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT EmpId, 2006, [2006] UNION ALL&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT EmpId, 2007, [2007]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ) AS s&lt;BR&gt;WHERE Sales IS NOT NULL&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;This query yields a nearly identical query plan.&amp;nbsp; The UNION ALL syntax produces a similar result to the constant scan except that there are now three constant scans and a concatenation operator:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Filter(WHERE:([Union1007] IS NOT NULL))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([p].[2005], [p].[2006], [p].[2007]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([tempdb].[dbo].[PIVOT_Sales] AS [p]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Concatenation&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((2005))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((2006))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((2007))))&lt;/P&gt;
&lt;P mce_keep="true"&gt;In this plan, [Union1007] is the &lt;I&gt;Sales&lt;/I&gt; column.&amp;nbsp; We can actually see the definition of [Union1007] from the &lt;I&gt;DefinedValues&lt;/I&gt; column of the concatenation operator in the SET SHOWPLAN_ALL ON output:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;[Union1006] = ([Expr1003], [Expr1004], [Expr1005]), [Union1007] = ([p].[2005], [p].[2006], [p].[2007])&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Notice that the values for [Union1007] are actually derived directly from the correlated parameters of the cross apply (from the &lt;EM&gt;PIVOT_Sales &lt;/EM&gt;table) and not from the constant scans.&amp;nbsp; [Union1006] is the &lt;EM&gt;Yr &lt;/EM&gt;column and the values are derived from the constant scans.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=3920521" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Pivot+and+Unpivot/default.aspx">Pivot and Unpivot</category></item><item><title>PIVOT Query Plans</title><link>http://blogs.msdn.com/craigfr/archive/2007/07/09/pivot-query-plans.aspx</link><pubDate>Mon, 09 Jul 2007 21:05:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:3784411</guid><dc:creator>craigfr</dc:creator><slash:comments>0</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/3784411.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=3784411</wfw:commentRss><description>&lt;P&gt;In &lt;A title="The PIVOT Operator" href="http://blogs.msdn.com/craigfr/archive/2007/07/03/the-pivot-operator.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2007/07/03/the-pivot-operator.aspx"&gt;my last post&lt;/A&gt;, I gave an overview of the PIVOT operator.&amp;nbsp; In this post, I'm going to take a look at the query plans generated by the PIVOT operator.&amp;nbsp; As we'll see, SQL Server generates a surprisingly simple query plan that is essentially just a fancy aggregation query plan.&lt;/P&gt;
&lt;P&gt;Let's use the same schema and queries from my previous post:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE Sales (EmpId INT, Yr INT, Sales MONEY)&lt;BR&gt;INSERT Sales VALUES(1, 2005, 12000)&lt;BR&gt;INSERT Sales VALUES(1, 2006, 18000)&lt;BR&gt;INSERT Sales VALUES(1, 2007, 25000)&lt;BR&gt;INSERT Sales VALUES(2, 2005, 15000)&lt;BR&gt;INSERT Sales VALUES(2, 2006, 6000)&lt;BR&gt;INSERT Sales VALUES(3, 2006, 20000)&lt;BR&gt;INSERT Sales VALUES(3, 2007, 24000)&lt;/P&gt;
&lt;P mce_keep="true"&gt;SELECT [2005], [2006], [2007]&lt;BR&gt;FROM (SELECT Yr, Sales FROM Sales) AS s&lt;BR&gt;PIVOT (SUM(Sales) FOR Yr IN ([2005], [2006], [2007])) AS p&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;This query generates the following query plan:&lt;/P&gt;&lt;PRE&gt;  |--Compute Scalar(DEFINE:(
                 [Expr1006]=CASE WHEN [Expr1024]=(0) THEN NULL ELSE [Expr1025] END,
                 [Expr1007]=CASE WHEN [Expr1026]=(0) THEN NULL ELSE [Expr1027] END,
                 [Expr1008]=CASE WHEN [Expr1028]=(0) THEN NULL ELSE [Expr1029] END))
       |--Stream Aggregate(DEFINE:(
                      [Expr1024]=COUNT_BIG(CASE WHEN [Sales].[Yr]=(2005) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1025]=SUM(CASE WHEN [Sales].[Yr]=(2005) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1026]=COUNT_BIG(CASE WHEN [Sales].[Yr]=(2006) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1027]=SUM(CASE WHEN [Sales].[Yr]=(2006) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1028]=COUNT_BIG(CASE WHEN [Sales].[Yr]=(2007) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1029]=SUM(CASE WHEN [Sales].[Yr]=(2007) THEN [Sales].[Sales] ELSE NULL END)))
            |--Table Scan(OBJECT:([Sales]))&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;This is just a basic &lt;A title=Aggregation href="http://blogs.msdn.com/craigfr/archive/2006/09/06/743116.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2006/09/06/743116.aspx"&gt;scalar aggregation&lt;/A&gt; query plan!&amp;nbsp; It calculates one SUM aggregate for each year.&amp;nbsp; Like any SUM aggregate, each aggregate actually computes both the count and the sum.&amp;nbsp; If the count is zero, the query plan returns NULL else it returns the sum.&amp;nbsp; (The compute scalar handles this logic.)&lt;/P&gt;
&lt;P&gt;The only twist is that each SUM aggregate is actually computed over a CASE statement that filter for those rows that match the year for which it is summing sales.&amp;nbsp; The CASE statement returns the value of the &lt;I&gt;Sales &lt;/I&gt;column for those rows that match the year and NULLs for all other rows.&amp;nbsp; To clarify what is happening, we can view the results of the CASE statements without the aggregation:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT EmpId, Yr,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CASE WHEN Yr = 2005 THEN Sales END AS [2005],&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CASE WHEN Yr = 2006 THEN Sales END AS [2006],&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CASE WHEN Yr = 2007 THEN Sales END AS [2007]&lt;BR&gt;FROM Sales&lt;/P&gt;&lt;/BLOCKQUOTE&gt;&lt;PRE&gt;EmpId       Yr          2005                  2006                  2007
----------- ----------- --------------------- --------------------- ---------------------
1           2005        12000.00              NULL                  NULL
1           2006        NULL                  18000.00              NULL
1           2007        NULL                  NULL                  25000.00
2           2005        15000.00              NULL                  NULL
2           2006        NULL                  6000.00               NULL
3           2006        NULL                  20000.00              NULL
3           2007        NULL                  NULL                  24000.00
2           2007        NULL                  NULL                  NULL&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;When computing the sums of each of the year columns, the query plan relies on the fact that aggregate functions discard NULLs; that is, the NULL values are not included in the results.&amp;nbsp; Although this point may seem intuitive for a SUM aggregate, the significance is clearer for a COUNT aggregate:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T (A INT)&lt;BR&gt;INSERT T VALUES(NULL)&lt;/P&gt;
&lt;P mce_keep="true"&gt;-- Returns 1: the number rows in table T&lt;BR&gt;SELECT COUNT(*) FROM T&lt;/P&gt;
&lt;P mce_keep="true"&gt;-- Returns 0: the number of non-NULL values of column A&lt;BR&gt;SELECT COUNT(A) FROM T&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;Note that we could just as easily have written the original query as:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SUM(CASE WHEN Yr = 2005 THEN Sales END) AS [2005],&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SUM(CASE WHEN Yr = 2006 THEN Sales END) AS [2006],&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SUM(CASE WHEN Yr = 2007 THEN Sales END) AS [2007]&lt;BR&gt;FROM Sales&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;This query gets a nearly identical query plan.&amp;nbsp; The only visible difference is the use of an extra compute scalar to evaluate the CASE statements.&lt;/P&gt;&lt;PRE&gt;  |--Compute Scalar(DEFINE:(
                 [Expr1004]=CASE WHEN [Expr1013]=(0) THEN NULL ELSE [Expr1014] END,
                 [Expr1005]=CASE WHEN [Expr1015]=(0) THEN NULL ELSE [Expr1016] END,
                 [Expr1006]=CASE WHEN [Expr1017]=(0) THEN NULL ELSE [Expr1018] END))
       |--Stream Aggregate(DEFINE:(
                      [Expr1013]=COUNT_BIG([Expr1007]), [Expr1014]=SUM([Expr1007]),
                      [Expr1015]=COUNT_BIG([Expr1008]), [Expr1016]=SUM([Expr1008]),
                      [Expr1017]=COUNT_BIG([Expr1009]), [Expr1018]=SUM([Expr1009])))
            |--Compute Scalar(DEFINE:(
                           [Expr1007]=CASE WHEN [Sales].[Yr]=(2005) THEN [Sales].[Sales] ELSE NULL END,
                           [Expr1008]=CASE WHEN [Sales].[Yr]=(2006) THEN [Sales].[Sales] ELSE NULL END,
                           [Expr1009]=CASE WHEN [Sales].[Yr]=(2007) THEN [Sales].[Sales] ELSE NULL END))
                 |--Table Scan(OBJECT:([Sales]))&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;The other big difference between the PIVOT syntax and query plan and the alternative syntax and query plan is that the PIVOT query suppresses the following warning about NULLs:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;Warning: Null value is eliminated by an aggregate or other SET operation.&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;We could also suppress this warning by executing the following statement:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SET ANSI_WARNINGS OFF&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;At this point, the query plan for a multi-row PIVOT operation should not come as a surprise:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT EmpId, [2005], [2006], [2007]&lt;BR&gt;FROM (SELECT EmpId, Yr, Sales FROM Sales) AS s&lt;BR&gt;PIVOT (SUM(Sales) FOR Yr IN ([2005], [2006], [2007])) AS p&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;The query plan for this PIVOT operation uses CASE statements to compute the same intermediate result that we saw above.&amp;nbsp; Then, like any other GROUP BY query, it uses either &lt;A title="Stream Aggregate" href="http://blogs.msdn.com/craigfr/archive/2006/09/13/752728.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2006/09/13/752728.aspx"&gt;stream&lt;/A&gt; or &lt;A title="Hash Aggregate" href="http://blogs.msdn.com/craigfr/archive/2006/09/20/hash-aggregate.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2006/09/20/hash-aggregate.aspx"&gt;hash aggregate&lt;/A&gt; to group by &lt;I&gt;EmpId&lt;/I&gt; and to compute the final result.&amp;nbsp; In this case, the optimizer chooses a stream aggregate.&amp;nbsp; Since we do not have an index to provide order, it must also introduce a sort.&lt;/P&gt;&lt;PRE&gt;  |--Compute Scalar(DEFINE:(
                 [Expr1007]=CASE WHEN [Expr1025]=(0) THEN NULL ELSE [Expr1026] END,
                 [Expr1008]=CASE WHEN [Expr1027]=(0) THEN NULL ELSE [Expr1028] END,
                 [Expr1009]=CASE WHEN [Expr1029]=(0) THEN NULL ELSE [Expr1030] END))
       |--Stream Aggregate(GROUP BY:([Sales].[EmpId]) DEFINE:(
                      [Expr1025]=COUNT_BIG(CASE WHEN [Sales].[Yr]=(2005) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1026]=SUM(CASE WHEN [Sales].[Yr]=(2005) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1027]=COUNT_BIG(CASE WHEN [Sales].[Yr]=(2006) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1028]=SUM(CASE WHEN [Sales].[Yr]=(2006) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1029]=COUNT_BIG(CASE WHEN [Sales].[Yr]=(2007) THEN [Sales].[Sales] ELSE NULL END),
                      [Expr1030]=SUM(CASE WHEN [Sales].[Yr]=(2007) THEN [Sales].[Sales] ELSE NULL END)))
            |--Compute Scalar(DEFINE:([Sales].[EmpId]=[Sales].[EmpId]))
                 |--Sort(ORDER BY:([Sales].[EmpId] ASC))
                      |--Table Scan(OBJECT:([Sales]))&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;To see how this query really is no different than any other GROUP BY query or to see some alternative query plans, try creating a clustered index on &lt;I&gt;EmpId&lt;/I&gt; to eliminate the sort or using an &lt;I&gt;OPTION(HASH GROUP)&lt;/I&gt; hint to force a hash aggregate.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=3784411" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Aggregation/default.aspx">Aggregation</category><category domain="http://blogs.msdn.com/craigfr/archive/tags/Pivot+and+Unpivot/default.aspx">Pivot and Unpivot</category></item><item><title>The PIVOT Operator</title><link>http://blogs.msdn.com/craigfr/archive/2007/07/03/the-pivot-operator.aspx</link><pubDate>Tue, 03 Jul 2007 21:31:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:3674230</guid><dc:creator>craigfr</dc:creator><slash:comments>6</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/3674230.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=3674230</wfw:commentRss><description>&lt;P&gt;In my next few posts, I'm going to look at how SQL Server implements the PIVOT and UNPIVOT operators.&amp;nbsp; Let's begin with the PIVOT operator.&amp;nbsp; The PIVOT operator takes a normalized table and transforms it into a new table where the columns of the new table are derived from the values in the original table.&amp;nbsp; For example, suppose we want to store annual sales data by employee.&amp;nbsp; We might create a schema such as the following:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE Sales (EmpId INT, Yr INT, Sales MONEY)&lt;BR&gt;INSERT Sales VALUES(1, 2005, 12000)&lt;BR&gt;INSERT Sales VALUES(1, 2006, 18000)&lt;BR&gt;INSERT Sales VALUES(1, 2007, 25000)&lt;BR&gt;INSERT Sales VALUES(2, 2005, 15000)&lt;BR&gt;INSERT Sales VALUES(2, 2006, 6000)&lt;BR&gt;INSERT Sales VALUES(3, 2006, 20000)&lt;BR&gt;INSERT Sales VALUES(3, 2007, 24000)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;Notice that this schema has one row per employee &lt;B&gt;per year&lt;/B&gt;.&amp;nbsp; Moreover, notice that in the sample data employees 2 and 3 only have sales data for two of the three years worth of data.&amp;nbsp; Now suppose that we'd like to transform this data into a table that has one row per employee &lt;B&gt;with all three years of sales data in each row&lt;/B&gt;.&amp;nbsp; We can achieve this conversion very easily using PIVOT:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT EmpId, [2005], [2006], [2007]&lt;BR&gt;FROM (SELECT EmpId, Yr, Sales FROM Sales) AS s&lt;BR&gt;PIVOT (SUM(Sales) FOR Yr IN ([2005], [2006], [2007])) AS p&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;I'm not going to delve into the PIVOT syntax which is already documented in &lt;A title="Using PIVOT and UNPIVOT" href="http://msdn2.microsoft.com/en-us/library/ms177410.aspx" mce_href="http://msdn2.microsoft.com/en-us/library/ms177410.aspx"&gt;Books Online&lt;/A&gt;.&amp;nbsp; Suffice it to say that this statement sums up the sales for each employee for each of the specified years and outputs one row per employee.&amp;nbsp; The resulting output is:&lt;/P&gt;&lt;PRE&gt;EmpId       2005                  2006                  2007
----------- --------------------- --------------------- ---------------------
1           12000.00              18000.00              25000.00
2           15000.00              6000.00               NULL
3           NULL                  20000.00              24000.00&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;Notice that SQL Server inserts NULLs for the missing sales data for employees 2 and 3.&lt;/P&gt;
&lt;P&gt;The SUM keyword (or some other aggregate) is required.&amp;nbsp; If the &lt;I&gt;Sales&lt;/I&gt; table includes multiple rows for a particular employee for a particular year, PIVOT does aggregate them - in this case by summing them -&amp;nbsp; into a single data point in the result.&amp;nbsp; Of course, in this example, since the entry in each "cell" of the output table is the result of summing a single input row, we could just as easily have used another aggregate such as MIN or MAX.&amp;nbsp; I've used SUM since it is more intuitive.&lt;/P&gt;
&lt;P&gt;This PIVOT example is reversible.&amp;nbsp; The information in the output table can be used to reconstruct the original input table using an UNPIVOT operation (which I will cover in a later post).&amp;nbsp; However, not all PIVOT operations are reversible.&amp;nbsp; To be reversible, a PIVOT operation must meet the following criteria:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;All of the input data must be transformed. If we include a filter of any kind including on the IN clause, some data may be omitted from the PIVOT result. For example, if we altered the above example only to output sales for 2006 and 2007, clearly we could not reconstruct the 2005 sales data from the result.&lt;/LI&gt;
&lt;LI&gt;Each cell in the output table must derive from a single input row. If multiple input rows are aggregated into a single cell, there is no way to reconstruct the original input rows.&lt;/LI&gt;
&lt;LI&gt;The aggregate function must be an identity function (when used on a single input row). SUM, MIN, MAX, and AVG all return the single input value unchanged and, thus, can be reversed. COUNT does not return its input value unchanged and, thus, cannot be reversed.&lt;/LI&gt;&lt;/UL&gt;
&lt;P&gt;Here is an example of a non-reversible PIVOT operation.&amp;nbsp; This example, calculates the total sales for all employees for all three years.&amp;nbsp; It does not itemize the output by employee.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT [2005], [2006], [2007]&lt;BR&gt;FROM (SELECT Yr, Sales FROM Sales) AS s&lt;BR&gt;PIVOT (SUM(Sales) FOR Yr IN ([2005], [2006], [2007])) AS p&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;Here is the output.&amp;nbsp; Each cell represents the sum of two or three rows from the input table.&lt;/P&gt;&lt;PRE&gt;2005                  2006                  2007
--------------------- --------------------- ---------------------
27000.00              44000.00              49000.00&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;In my next post, I'll look at some example PIVOT query plans.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=3674230" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Aggregation/default.aspx">Aggregation</category><category domain="http://blogs.msdn.com/craigfr/archive/tags/Pivot+and+Unpivot/default.aspx">Pivot and Unpivot</category></item></channel></rss>