6

I have read dozens of solutions to similar transposition problems as the one I am about to propose but oddly none that exactly mirrors my issue. I am simply trying to flip my rows to columns in a simple dashboard type data set.

The data when pulled from various transaction tables looks like this:

DatePeriod  PeriodNumberOverall   Transactions   Customers   Visits

'Jan 2012'   1                    100            50          150
'Feb 2012'   2                    200            100         300
'Mar 2012'   3                    300            200         600

and I want to be able to generate the following:

                      Jan 2012   Feb 2012   Mar 2012

Transactions          100        200        300
Customers             50         100        200
Visits                150        300        600

The metrics will be static (Transactions, Customers and Visits), but the date periods will be dynamic (IE - more added as months go by).

Again, I have ready many examples leveraging pivot, unpivot, store procedures, UNION ALLs, etc, but nothing where I am not doing any aggregating, just literally transposing the whole output. I have also found an easy way to do this in Visual Studio 2005 using a matrix with an embedded list, but I can't export the final output to excel which is a requirement. Any help would be greatly appreciated.

Taryn
  • 242,637
  • 56
  • 362
  • 405
user2234794
  • 63
  • 1
  • 1
  • 3
  • Check Dynamic Pivot Version in Bluefeet's answert at http://stackoverflow.com/questions/13372276/simple-way-to-transpose-columns-and-rows-in-sql – Nenad Zivkovic Apr 02 '13 at 07:22

3 Answers3

2

You need to dynamically create a SQL statement with PIVOT and APPLY operators on the fly and then run that command. If your metrics static(Transactions, Customers and Visits), hence we can use CROSS APPLY operator with VALUES As a Table Source.

For SQL Server2008+

DECLARE @cols nvarchar( max),
        @query nvarchar(max)
SELECT @cols =
  STUFF((SELECT ',' + QUOTENAME(t.DatePeriod) AS ColName                               
         FROM dbo.test62 t                   
         FOR XML PATH(''), TYPE).value ('.', 'nvarchar(max)'), 1, 1, '')     

SET @query =
 'SELECT *
  FROM (
        SELECT t.DatePeriod, COALESCE(o.Transactions, o.Customers, o.Visits) AS PvtVals, o.PvtColumns, o.OrderColumns 
        FROM dbo.test62 t CROSS APPLY (
                                       VALUES(t.Transactions, NULL, NULL, ''Transaction'', 1),
                                             (NULL, t.Customers, NULL, ''Customers'', 2),
                                             (NULL, NULL, t.Visits, ''Visits'', 3)
                                       ) o (Transactions, Customers, Visits, PvtColumns, OrderColumns)
        ) p
  PIVOT
   (      
    MAX(PvtVals) FOR DatePeriod IN (' + @cols + ')
    ) AS pvt
  ORDER BY pvt.OrderColumns '
EXEC(@query) 

Result:

PvtColumns  Jan 2012 Fed 2012 Mar 2012
Transaction 100      200      300
Customers   50       100      200
Visits      150      300      600

Demo on SQLFiddle

For SQL Server 2005

DECLARE @cols nvarchar( max),
        @query nvarchar(max)
SELECT @cols =
  STUFF((SELECT ',' + QUOTENAME(t.DatePeriod) AS ColName                               
         FROM dbo.test62 t                   
         FOR XML PATH(''), TYPE).value ('.', 'nvarchar(max)'), 1, 1, '')     

SET @query =
 'SELECT *
  FROM (
        SELECT t.DatePeriod, COALESCE(o.Transactions, o.Customers, o.Visits) AS PvtVals, o.PvtColumns, o.OrderColumns
        FROM dbo.test62 t CROSS APPLY (
                                       SELECT t.Transactions, NULL, NULL, ''Transaction'', 1
                                       UNION ALL 
                                       SELECT NULL, t.Customers, NULL, ''Customers'', 2
                                       UNION ALL 
                                       SELECT NULL, NULL, t.Visits, ''Visits'', 3
                                       ) o (Transactions, Customers, Visits, PvtColumns, OrderColumns)
        ) p
  PIVOT
   (      
    MAX(PvtVals) FOR DatePeriod IN (' + @cols + ')
    ) AS pvt
  ORDER BY pvt.OrderColumns'
EXEC(@query) 
Aleksandr Fedorenko
  • 16,594
  • 6
  • 37
  • 44
  • 1
    `CROSS APPLY ... VALUES` doesn't work in 2005. You can use `CROSS APPLY (SELECT t.Transactions, NULL, NULL, ''Transaction'' UNION ALL SELECT NULL, t.Customers, NULL, ''Customers'' UNION ALL ... )` though – Martin Smith Apr 02 '13 at 07:40
  • Thanks Martin for a clarifications!;)Answer updated. BTW, If use CROSS APPLY (SELECT Transactions, Customers, Visits, PvtColumns FROM (VALUES (t.Transactions, NULL, NULL, ''Transaction''), (NULL, t.Customers, NULL, ''Customers''), (NULL, NULL, t.Visits, ''Visits'')) x(Transactions, Customers, Visits, PvtColumns)) o ... it will work? – Aleksandr Fedorenko Apr 02 '13 at 08:00
  • Thanks so much for the awesome answers! The only question I have that remains is there any easy way to order the metrics how I want them? In all three answers the metrics got reordered in alpha order. Thanks again! – user2234794 Apr 02 '13 at 15:11
2

In order to get the result that you want you need to first UNPIVOT the data and then PIVOT theDatePeriod` Values.

The UNPIVOT will transform the multiple columns of Transactions, Customers and Visits into multiple rows. The other answers are using a UNION ALL to unpivot but SQL Server 2005 was the first year the UNPIVOT function was supported.

The query to unpivot the data is:

select dateperiod,
  col, value
from transactions
unpivot
(
  value for col in (Transactions, Customers, Visits)
) u

See Demo. This transforms your current columns into multiple rows, so the data looks like the following:

| DATEPERIOD |          COL | VALUE |
-------------------------------------
|   Jan 2012 | Transactions |   100 |
|   Jan 2012 |    Customers |    50 |
|   Jan 2012 |       Visits |   150 |
|   Feb 2012 | Transactions |   200 |

Now, since the data is in rows, you can apply the PIVOT function to the DatePeriod column:

select col, [Jan 2012], [Feb 2012], [Mar 2012]
from
(
  select dateperiod,
    t.col, value, c.SortOrder
  from
  (
    select dateperiod,
      col, value
    from transactions
    unpivot
    (
      value for col in (Transactions, Customers, Visits)
    ) u
  ) t
  inner join
  (
    select 'Transactions' col, 1 SortOrder
    union all
    select 'Customers' col, 2 SortOrder
    union all
    select 'Visits' col, 3 SortOrder
   ) c
    on t.col = c.col
) d
pivot
(
  sum(value)
  for dateperiod in ([Jan 2012], [Feb 2012], [Mar 2012])
) piv
order by SortOrder;

See SQL Fiddle with Demo.

If you have an unknown number of date period's then you will use dynamic SQL:

DECLARE @cols AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX)

select @cols = STUFF((SELECT ',' + QUOTENAME(dateperiod) 
                    from transactions
                    group by dateperiod, PeriodNumberOverall
                    order by PeriodNumberOverall
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')

set @query = 'SELECT col, ' + @cols + ' 
             from 
             (
                select dateperiod,
                  t.col, value, c.SortOrder
                from
                (
                  select dateperiod,
                    col, value
                  from transactions
                  unpivot
                  (
                    value for col in (Transactions, Customers, Visits)
                  ) u
                ) t
                inner join
                (
                  select ''Transactions'' col, 1 SortOrder
                  union all
                  select ''Customers'' col, 2 SortOrder
                  union all
                  select ''Visits'' col, 3 SortOrder
                 ) c
                  on t.col = c.col
            ) x
            pivot 
            (
                sum(value)
                for dateperiod in (' + @cols + ')
            ) p 
            order by SortOrder'

execute(@query)

See SQL Fiddle with Demo. Both will give the result:

|          COL | JAN 2012 | FEB 2012 | MAR 2012 |
-------------------------------------------------
| Transactions |      100 |      200 |      300 |
|    Customers |       50 |      100 |      200 |
|       Visits |      150 |      300 |      600 |
Taryn
  • 242,637
  • 56
  • 362
  • 405
  • Thanks so much for the awesome answers! The only question I have that remains is there any easy way to order the metrics how I want them? In all three answers the metrics got reordered in alpha order. Thanks again! – user2234794 Apr 02 '13 at 15:11
  • @user2234794 See my edit, I updated my answer to include an ordering my the columns initial positions – Taryn Apr 02 '13 at 15:23
  • Thank you for the update. The main "table" I will be using to transpose for my dashboard will not be a part of the schema - it will be data elements (aggregated metrics/fields) pulled from multiple other tables) into a temp table. Is there another way to do the ordering if I am not pulling my fields from a single table that exists in my schema? Apologies if the question is simple/basic - I am not a T-SQL expert by any measure. – user2234794 Apr 02 '13 at 15:52
  • @user2234794 Yes, you can join on a derived table containing a sortorder. See my edit – Taryn Apr 02 '13 at 15:56
1

If you can know how many different date period in advance, then you can use fixed query like following:


;with CTE_UNIONTable
as 
(
select [DatePeriod],[PeriodNumberOverall],[Transactions] as [value], 'Transactions' as subType from table1
UNION ALL 
select [DatePeriod],[PeriodNumberOverall],[Customers] as [value], 'Customers' as subType from table1
UNION ALL 
select [DatePeriod],[PeriodNumberOverall],[Visits] as [value], 'Visits' as subType from table1
), CTE_MiddleResult
as 
(
select * from CTE_UNIONTable
    pivot 
    (
       max(value) 
       for DatePeriod in ([Jan 2012],[Feb 2012],[Mar 2012])
     ) as P
     )
select SubType, max([Jan 2012]) as [Jan 2012] ,max([Feb 2012]) as [Feb 2012], max([Mar 2012]) as [Feb 2012]
from CTE_MiddleResult
group by SubType

SQL FIDDLE DEMO

If how many date period is unpredictable, then @Alexander already gave the solution, the following code is just a second opinion, instead of using APPLY, using UNION ALL


DECLARE @cols nvarchar( max),
        @query nvarchar (max),
        @selective nvarchar(max)
SELECT @cols =
  STUFF((SELECT ',' + QUOTENAME(t.DatePeriod) AS ColName                               
         FROM table1 t                   
         FOR XML PATH( ''), TYPE).value ('.', 'nvarchar(max)'),1,1,'')

SELECT @selective =
  STUFF((SELECT ',MAX(' + QUOTENAME(t.DatePeriod) +') as ' + QUOTENAME(t.DatePeriod)  AS ColName                               
         FROM table1 t                   
         FOR XML PATH( ''), TYPE).value ('.', 'nvarchar(max)'),1,1,'')

set @query = '
;with CTE_UNIONTable
as 
(
select [DatePeriod],[PeriodNumberOverall],[Transactions] as [value], ''Transactions'' as subType from table1
UNION ALL 
select [DatePeriod],[PeriodNumberOverall],[Customers] as [value], ''Customers'' as subType from table1
UNION ALL 
select [DatePeriod],[PeriodNumberOverall],[Visits] as [value], ''Visits'' as subType from table1
), CTE_MiddleResult
as 
(
select * from CTE_UNIONTable
    pivot 
    (
       max(value) 
       for DatePeriod in ('+@cols+')
     ) as P
)
select SubType,' + @selective + ' 
from CTE_MiddleResult
group by SubType'

exec(@query)

SQL FIDDLE DEMO

ljh
  • 2,546
  • 1
  • 14
  • 20
  • Thanks so much for the awesome answers! The only question I have that remains is there any easy way to order the metrics how I want them? In all three answers the metrics got reordered in alpha order. Thanks again! – user2234794 Apr 02 '13 at 15:10
  • You can add ORDER BY in the end, but either asc or desc, what exact order do you want, maybe the code needs to update a little bit basing on the order you want. – ljh Apr 02 '13 at 16:00