Pivot query to return multiple repeating groups?

pivot, sql-server, sql-server-2008, t-sql, unpivot

Solution

You can get the result that you want using both `UNPIVOT` and `PIVOT`:

select *
from
(
  select storekey, 
    value, col +'Last'+ cast(numberofdaysback as varchar(20)) + 'Days' new_col
  from
  (
    select storekey,
      numberofdaysback,
      cast(ThisYearGrossTransactions as decimal(20,5)) ThisYearGrossTransactions,
      cast(ThisYearGrossPrice as decimal(20,5)) ThisYearGrossPrice,
      cast(LastYearGrossTransactions as decimal(20,5)) LastYearGrossTransactions,
      cast(LastYearGrossPrice as decimal(20,5)) LastYearGrossPrice    
    from aggregate
  ) un
  unpivot
  (
    value
    for col in (ThisYearGrossTransactions, ThisYearGrossPrice,
                LastYearGrossTransactions, LastYearGrossPrice)
  ) unpiv
) src
pivot
(
  sum(value)
  for new_col in ([ThisYearGrossTransactionsLast7Days], [ThisYearGrossPriceLast7Days],
                  [LastYearGrossTransactionsLast7Days], [LastYearGrossPriceLast7Days],
                  [ThisYearGrossTransactionsLast28Days], [ThisYearGrossPriceLast28Days],
                  [LastYearGrossTransactionsLast28Days], [LastYearGrossPriceLast28Days])
) piv;

See SQL Fiddle with Demo

The `UNPIVOT` takes the column values in `ThisYearGrossTransactions`, `ThisYearGrossPrice`, `LastYearGrossTransactions` and `LastYearGrossPrice` and converts them into a single column with multiple rows.

select storekey, 
  value, col +'Last'+ cast(numberofdaysback as varchar(20)) + 'Days' new_col
from
(
  select storekey,
    numberofdaysback,
    cast(ThisYearGrossTransactions as decimal(20,5)) ThisYearGrossTransactions,
    cast(ThisYearGrossPrice as decimal(20,5)) ThisYearGrossPrice,
    cast(LastYearGrossTransactions as decimal(20,5)) LastYearGrossTransactions,
    cast(LastYearGrossPrice as decimal(20,5)) LastYearGrossPrice    
  from aggregate
) un
unpivot
(
  value
  for col in (ThisYearGrossTransactions, ThisYearGrossPrice,
              LastYearGrossTransactions, LastYearGrossPrice)
) unpiv

See SQL Fiddle with Demo

A requirement of the `UNPIVOT` is that all of the datatypes must be the same so you need to apply either `cast` or `convert` to any values. Then to `PIVOT` the data, I created the new column names by adding the `numberofdaysback` to each record. THese are the values that are used in the `PIVOT` portion of the query.

The final result is:

| STOREKEY | THISYEARGROSSTRANSACTIONSLAST7DAYS | THISYEARGROSSPRICELAST7DAYS | LASTYEARGROSSTRANSACTIONSLAST7DAYS | LASTYEARGROSSPRICELAST7DAYS | THISYEARGROSSTRANSACTIONSLAST28DAYS | THISYEARGROSSPRICELAST28DAYS | LASTYEARGROSSTRANSACTIONSLAST28DAYS | LASTYEARGROSSPRICELAST28DAYS |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|    10130 |                                  6 |                      429.31 |                                 15 |                     1606.11 |                                  62 |                    6427.7072 |                                  93 |                        13428 |
|    10131 |                                  9 |                     1299.83 |                                  3 |                      662.57 |                                  29 |                      5725.49 |                                   8 |                      1938.41 |
|    10132 |                                 57 |                    11029.53 |                                 56 |                     6848.38 |                                 262 |                   42892.5476 |                                 269 |                     37229.26 |
|    10134 |                                198 |                    71324.36 |                                248 |                  95889.6089 |                                 815 |                  339315.9265 |                                 822 |                  342834.2365 |

The static version above works great if you have a known number of values for `NumberOfDaysBack` but if you have an unknown number of many values, then you can use a dynamic version of this:

DECLARE @colsUnpivot AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX),
    @colsPivot as  NVARCHAR(MAX)

select @colsUnpivot = stuff((select ','+quotename(C.name)
         from sys.columns as C
         where C.object_id = object_id('Aggregate') and
               C.name not in ('StoreKey', 'NumberOfDaysBack')
         for xml path('')), 1, 1, '')

select @colsPivot = STUFF((SELECT  ',' 
                      + quotename(c.name +'Last'
                         + cast(a.NumberOfDaysBack as varchar(10)) +'Days')
                    from Aggregate a
                    cross apply sys.columns  C
                   where C.object_id = object_id('Aggregate') and
                         C.name not in ('StoreKey', 'NumberOfDaysBack')
                   group by c.name, a.NumberOfDaysBack
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')

set @query 
  = 'select *
      from
      (
        select storekey, 
            value, col +''Last''+ cast(numberofdaysback as varchar(20)) + ''Days'' new_col
        from 
        (
          select storekey,
            numberofdaysback,
            cast(ThisYearGrossTransactions as decimal(20,5)) ThisYearGrossTransactions,
            cast(ThisYearGrossPrice as decimal(20,5)) ThisYearGrossPrice,
            cast(LastYearGrossTransactions as decimal(20,5)) LastYearGrossTransactions,
            cast(LastYearGrossPrice as decimal(20,5)) LastYearGrossPrice    
          from aggregate
        ) x
        unpivot
        (
          value
          for col in ('+ @colsunpivot +')
        ) u
      ) x1
      pivot
      (
        sum(value)
        for new_col in ('+ @colspivot +')
      ) p'

exec(@query)

See SQL Fiddle with Demo

The result will be the same with both queries.

Problem

I'm trying to get a result set (which will be inserted into a table) that has multiple repeating groups. Here's a script that shows a very simplified version of the data I'm starting out with: ``` CREATE TABLE #Aggregate( StoreKey int , NumberOfDaysBack int , ThisYearGrossTransactions int , ThisYearGrossPrice money , LastYearGrossTransactions int , LastYearGrossPrice money ) GO INSERT #Aggregate VALUES (10134, 7, 198, 71324.3600, 248, 95889.6089) INSERT #Aggregate VALUES (10131, 7, 9, 1299.8300, 3, 662.5700) INSERT #Aggregate VALUES (10132, 7, 57, 11029.5300, 56, 6848.3800) INSERT #Aggregate VALUES (10130, 7, 6, 429.3100, 15, 1606.1100) INSERT #Aggregate VALUES (10134, 28, 815, 339315.9265, 822, 342834.2365) INSERT #Aggregate VALUES (10131, 28, 29, 5725.4900, 8, 1938.4100) INSERT #Aggregate VALUES (10132, 28, 262, 42892.5476, 269, 37229.2600) INSERT #Aggregate VALUES (10130, 28, 62, 6427.7072, 93, 13428.0000) ``` And then I'd like to show separate sets of data for each set of NumberOfDaysBack, like this: ``` StoreKey ThisYearLast7GrossTransactions ThisYearLast7GrossPrice LastYearLast7GrossTransactions LastYearLast7GrossPrice ThisYearLast28GrossTransactions ThisYearLast28GrossPrice LastYearLast28GrossTransactions LastYearLast28GrossPrice ----------- ------------------------------ ----------------------- ------------------------------ ----------------------- ------------------------------- ------------------------ ------------------------------- ------------------------ 10130 6 429.31 15 1606.11 62 6427.7072 93 13428.00 10131 9 1299.83 3 662.57 29 5725.49 8 1938.41 10132 57 11029.53 56 6848.38 262 42892.5476 269 37229.26 10134 198 71324.36 248 95889.6089 815 339315.9265 822 342834.2365 ``` I was able to get the above result set with this query. ``` -- (using this Common Table expression as a shortcut, there's actually a dimention table ;with Store as (select distinct StoreKey from #Aggregate) Select Store.StoreKey ,ThisYearLast7GrossTransactions = DaysBack7.ThisYearGrossTransactions ,ThisYearLast7GrossPrice = DaysBack7.ThisYearGrossPrice ,LastYearLast7GrossTransactions = DaysBack7.LastYearGrossTransactions ,LastYearLast7GrossPrice = DaysBack7.LastYearGrossPrice ,ThisYearLast28GrossTransactions = DaysBack28.ThisYearGrossTransactions ,ThisYearLast28GrossPrice = DaysBack28.ThisYearGrossPrice ,LastYearLast28GrossTransactions = DaysBack28.LastYearGrossTransactions ,LastYearLast28GrossPrice = DaysBack28.LastYearGrossPrice from Store join #Aggregate DaysBack7 on Store .StoreKey = DaysBack7.StoreKey and DaysBack7 .NumberOfDaysBack = 7 join #Aggregate DaysBack28 on Store .StoreKey = DaysBack28.StoreKey and DaysBack28 .NumberOfDaysBack = 28 order by Store.StoreKey ``` However, since my actual data set is far more complicated, with many more NumberOfDaysBack and many more metrics that may change, I'd like to be able to do this with a pivot statement, without needing to explicitly name each field. Is this possible? Thanks for any ideas!

Original source

Related problems