T-SQL Pivot - Total Row and Dynamic Columns

2019-02-26 06:22发布

Let's jump straight into it. Here's the code

SELECT [prov], [201304], [201305], [201306], [201307]
FROM (
SELECT [prov], [arrival], [Amount]
FROM [tblSource]) up
PIVOT (SUM([Amount]) FOR [arrival] IN ([201304], [201305], [201306], [201307])) AS pvt
GO

It brings me back an ever so lovely table. I was wondering how I would get the totals for each "date" column to show in an appended last row?

In addition, the underlying table will have more data added, specifically more dates. This means that 201308 will be added next, then 201309 etc

This will mean that currently I will have to amend the code above each month to reflect the addition. Is there anyway around this?

2条回答
ゆ 、 Hurt°
2楼-- · 2019-02-26 07:01

You can dynamically create the columns using dynamic SQL, however, I would really recommend handling dynamic pivots in a layer designed for it, such as SSRS or excel.

DECLARE @SQL NVARCHAR(MAX) = '',
        @SQL2 NVARCHAR(MAX) = '',
        @SQL3 NVARCHAR(MAX) = '';

-- COMPILE THE UNIQUE VALUES FOR ARRIVAL THAT NEED TO BE PIVOTED
SELECT  @SQL = @SQL + ',' + QUOTENAME(Arrival),
        @SQL2 = @SQL2 + '+ISNULL(' + QUOTENAME(Arrival) + ', 0)',
        @SQL3 = @SQL3 + ',' + QUOTENAME(Arrival) + ' = ISNULL(' + QUOTENAME(Arrival) + ', 0)'
FROM    (SELECT DISTINCT Arrival FROM tblSource) s;

-- COMBINE THEM INTO A SINGLE QUERY
SET @SQL = 'SELECT [Prov]' + @SQL3 + ', [Total] = ' + STUFF(@SQL2, 1, 1, '') + '
            FROM    (   SELECT  Arrival, Prov, Amount
                        FROM    [tblSource]
                        UNION ALL
                        SELECT  Arrival, ''Total'', SUM(Amount)
                        FROM    [tblSource]
                        GROUP BY Arrival
                    ) up
                    PIVOT
                    (   SUM(Amount)
                        FOR Arrival IN (' + STUFF(@SQL, 1, 1, '') + ')
                    ) pvt;';

-- EXECUTE THE QUERY
EXECUTE SP_EXECUTESQL @SQL;

This creates and executes the following SQL:

SELECT  [Prov],
        [2013-01-01] = ISNULL([2013-01-01], 0),
        [2013-02-01] = ISNULL([2013-02-01], 0), 
        [Total] = ISNULL([2013-01-01], 0) + ISNULL([2013-02-01], 0)
FROM    (   SELECT  Arrival, Prov, Amount
            FROM    [tblSource]
            UNION ALL
            SELECT  Arrival, 'Total', SUM(Amount)
            FROM    [tblSource]
            GROUP BY Arrival
        ) up
        PIVOT
        (   SUM(Amount)
            FOR Arrival IN ([2013-01-01],[2013-02-01])
        ) pvt;

It is the query below union in the subquery up that adds the total row at the bottom, and the row total is simply created by adding all the columns in the row.

Example on SQL Fiddle

I will stress again though, I really recommend handling manipulation of data like this outside of SQL.

EDIT

An alternative to using the UNION to get the the total row is to use GROUPING SETS as follows:

DECLARE @SQL NVARCHAR(MAX) = '',
        @SQL2 NVARCHAR(MAX) = '',
        @SQL3 NVARCHAR(MAX) = '';

-- COMPILE THE UNIQUE VALUES FOR ARRIVAL THAT NEED TO BE PIVOTED
SELECT  @SQL = @SQL + ',' + QUOTENAME(Arrival),
        @SQL2 = @SQL2 + '+ISNULL(' + QUOTENAME(Arrival) + ', 0)',
        @SQL3 = @SQL3 + ',' + QUOTENAME(Arrival) + ' = ISNULL(' + QUOTENAME(Arrival) + ', 0)'
FROM    (SELECT DISTINCT Arrival FROM tblSource) s;

-- COMBINE THEM INTO A SINGLE QUERY
SET @SQL = 'SELECT [Prov]' + @SQL3 + ', [Total] = ' + STUFF(@SQL2, 1, 1, '') + '
            FROM    (   SELECT  Arrival, Prov = ISNULL(Prov, 'Total'), Amount = SUM(Amount)
                        FROM    [tblSource]
                        GROUP BY GROUPING SETS((Prov, arrival), (arrival))
                    ) up
                    PIVOT
                    (   SUM(Amount)
                        FOR Arrival IN (' + STUFF(@SQL, 1, 1, '') + ')
                    ) pvt;';

-- EXECUTE THE QUERY
EXECUTE SP_EXECUTESQL @SQL;
查看更多
仙女界的扛把子
3楼-- · 2019-02-26 07:22

SAMPLE TABLE

CREATE TABLE #TEMP([prov] VARCHAR(100),[arrival] INT, AMOUNT NUMERIC(12,2))

INSERT INTO #TEMP
SELECT 'A' [prov],'201304' [arrival],100 AMOUNT
UNION ALL
SELECT 'A' ,'201305' ,124 
UNION ALL
SELECT 'A' ,'201306' ,156
UNION ALL
SELECT 'B' ,'201304' ,67 
UNION ALL
SELECT 'B' ,'201305' ,211 
UNION ALL
SELECT 'B' ,'201306' ,176 
UNION ALL
SELECT 'C' ,'201304' ,43 
UNION ALL
SELECT 'C' ,'201305' ,56 
UNION ALL
SELECT 'C' ,'201306' ,158

QUERY

You can use ROLLUP to get the row total. More about ROLLUP here

-- Get the columns for dynamic pivot
DECLARE @cols NVARCHAR (MAX)

SELECT @cols = COALESCE (@cols + ',[' + CAST([arrival] AS VARCHAR(50)) + ']', 
                '[' + CAST([arrival] AS VARCHAR(50)) + ']')
               FROM (SELECT DISTINCT [arrival] FROM  #TEMP) PV 
               ORDER BY [arrival] 

-- Replace NULL value with zero
DECLARE @NulltoZeroCols NVARCHAR (MAX)

SELECT @NullToZeroCols = SUBSTRING((SELECT ',ISNULL(['+[arrival]+'],0) AS ['+[arrival]+']' 
FROM (SELECT DISTINCT CAST([arrival] AS VARCHAR(50)) [arrival] FROM #TEMP)TAB  
ORDER BY CAST([arrival]AS INT) FOR XML PATH('')),2,8000) 


DECLARE @query NVARCHAR(MAX)
SET @query = 'SELECT [prov],' + @NullToZeroCols + ' FROM 
             (
                 SELECT 
                 ISNULL([prov],''Total'')[prov], 
                 SUM(AMOUNT)AMOUNT , 
                 ISNULL(CAST([arrival] AS VARCHAR(50)),''Total'')[arrival]             
                 FROM #TEMP                 
                 GROUP BY [arrival],[prov]
                 WITH ROLLUP
             ) x
             PIVOT 
             (
                 MIN(AMOUNT)
                 FOR [arrival] IN (' + @cols + ')
            ) p
            ORDER BY CASE WHEN ([prov]=''Total'') THEN 1 ELSE 0 END,[prov]' 

EXEC SP_EXECUTESQL @query

Note : If you do not want to replace NULL with zero, just replace @NullToZeroCols with @cols in outer query of dynamic pivot

查看更多
登录 后发表回答