28

I am facing the following challenge. I need to rotate table data twice over the same column. Here's a screenshot of the data.

A list of items with a purchasing and a selling value for each year the items were sold

I want to have one row for each Item ID containing both the purchasing value and the selling value for each year. I tried doing this by selecting the "year" column twice, formatting it a bit so each selling year gets prefixed with a "S" and each purchasing year begins with a "P", and using 2 pivots to rotate around the 2 year columns. Here's the SQL query (used in SQL Server 2008):

SELECT [Item ID], 
        [P2000],[P2001],[P2002],[P2003],
        [S2000],[S2001],[S2002],[S2003]
FROM 
(

SELECT [Item ID]
      ,'P' + [Year] AS YearOfPurchase
      ,'S' + [Year] AS YearOfSelling

  ,[Purchasing value]
  ,[Selling value]
  FROM [ItemPrices]
) AS ALIAS

PIVOT 
(
MIN ([Purchasing value]) FOR [YearOfPurchase] in ([P2000],[P2001],[P2002],[P2003])
)
AS pvt

PIVOT 
(
MIN ([Selling value]) FOR [YearOfSelling] in ([S2000],[S2001],[S2002],[S2003])
)
AS pvt2

The result is not exactly what I was hoping for (see image below):

Actual situation: Too many rows

As you can see, there are still more than one row for each item ID. Is there a way to reduce the number of rows to exactly one per item? So that it looks a bit like the Excel screenshot below?

Desired situation: One row for each item ID

Taryn
  • 242,637
  • 56
  • 362
  • 405
Rob Vermeulen
  • 1,910
  • 1
  • 15
  • 22

3 Answers3

33

My suggestion would be to apply both the UNPIVOT and the PIVOT functions to get the result.

The UNPIVOT will turn the PurchasingValue and SellingValue columns into rows. Once this is done, then you can pivot the data into your result.

The code will be:

select *
from
(
  select itemid, 
    case 
      when col = 'PurchasingValue' then 'P'
      when col = 'SellingValue' then 'S'
    end + cast(year as varchar(4)) new_col,
    value
  from yourtable
  unpivot
  (
    value
    for col in ([PurchasingValue], [SellingValue])
  ) unpiv
) src
pivot
(
  max(value)
  for new_col in (P2000, P2001, P2002, P2003,
                  S2000, S2001, S2002, S2003)
) piv;

See SQL Fiddle with Demo. The result is:

| ITEMID | P2000 | P2001 | P2002 | P2003 | S2000 | S2001 | S2002 | S2003 |
--------------------------------------------------------------------------
|      1 |  1000 |  1100 |  1200 |  1300 |   900 |   990 |  1080 |  1170 |
|      2 |   500 |   550 |   600 |   650 |   450 |   495 |   540 |   585 |

In SQL Server 2008+ you can use CROSS APPLY with VALUES along with the PIVOT function:

select *
from
(
  select itemid,
    col+cast(year as varchar(4)) new_col,
    value
  from yourtable
  cross apply
  (
    VALUES
        (PurchasingValue, 'P'),
        (SellingValue, 'S')
   ) x (value, col)
) src
pivot
(
  max(value)
  for new_col in (P2000, P2001, P2002, P2003,
                  S2000, S2001, S2002, S2003)
) piv

See SQL Fiddle with Demo

Taryn
  • 242,637
  • 56
  • 362
  • 405
  • Thanks for the unpivot suggestion. That hadn't crossed my mind (and not sure if it would have in the future :-) ) – Rob Vermeulen Mar 07 '13 at 20:35
  • 1
    @RobVermeulen Yeah the unpivot function works great. I just updated my answer with a version that uses `cross apply` as well. The both will get the same result. – Taryn Mar 07 '13 at 20:41
  • Would "cross apply" be faster? – Rob Vermeulen Oct 29 '13 at 08:21
  • 1
    @RobVermeulen That is hard to say, it really depends on many factors. If you have concerns about speed, then you should try different variations of the query to determine what will work best for your situation. – Taryn Oct 29 '13 at 12:05
  • 1
    i know thank you posts are unwelcome, but i just had to tell you a great thank you! you have saved my week sir. before reaching this post the only solution i found was to pivot twice on the same table and then join them, but for some reason the second pivot was running on the cartesian product of the join rather than on the table itself, so as soon as the number of lines exceeded 1000 the execeution time rocketed sky high. so it's only after knowing of the existence of unpivot (thanks to you) that i was able to solve my problem. cheers! – Souhaieb Besbes May 07 '15 at 08:01
  • As I look at this answer for my own use, would I be correct in saying that this method could only be used as long as the aggregation function you plan on applying to both types of values are the same? Lets say you wanted to `SUM` the Purchase value, but you wanted to `AVG` the Selling value. I imagine the best answer would be to stick with OP's code and use the logic that @pieterGeerkens suggests? – JHixson Sep 22 '17 at 02:02
  • @JHixson if you need to use different types of aggregation , then you'd want to use a case expression along with the aggregate function similar to JamieD77's answer – Taryn Sep 22 '17 at 02:06
  • Another time you can't use this is when the two columns you want to pivot out are different types :(. If you follow this path, you end up with an error like `The type of column "some_float_column" conflicts with the type of other columns specified in the UNPIVOT list.` – Alain Dec 17 '20 at 00:06
  • 1
    @Alain if you use the CROSS APPLY version then you can cast/convert the values to the same data types. – Taryn Dec 17 '20 at 00:08
11

One easy way to pivot multiple columns is to just use Aggregate(Case) expressions.

SELECT  [Item ID],
        [P2000] = SUM(CASE WHEN [Year] = 2000 THEN [Purchasing value] END),
        [P2001] = SUM(CASE WHEN [Year] = 2001 THEN [Purchasing value] END),
        [P2002] = SUM(CASE WHEN [Year] = 2002 THEN [Purchasing value] END),
        [P2003] = SUM(CASE WHEN [Year] = 2003 THEN [Purchasing value] END),
        [S2000] = SUM(CASE WHEN [Year] = 2000 THEN [Selling value] END),
        [S2001] = SUM(CASE WHEN [Year] = 2001 THEN [Selling value] END),
        [S2002] = SUM(CASE WHEN [Year] = 2002 THEN [Selling value] END),
        [S2003] = SUM(CASE WHEN [Year] = 2003 THEN [Selling value] END)
FROM    ItemPrices
GROUP BY [Item ID]
JamieD77
  • 13,796
  • 1
  • 17
  • 27
5

Use a GROUP BY ItemID, with aggregate function SUM(isnull(value,0)) on each of the results columns.

Pieter Geerkens
  • 11,775
  • 2
  • 32
  • 52