【问题标题】:SQL Server dynamic PIVOT query?SQL Server 动态 PIVOT 查询?
【发布时间】:2012-05-11 08:56:23
【问题描述】:

我的任务是想出一种翻译以下数据的方法:

date        category        amount
1/1/2012    ABC             1000.00
2/1/2012    DEF             500.00
2/1/2012    GHI             800.00
2/10/2012   DEF             700.00
3/1/2012    ABC             1100.00

如下:

date        ABC             DEF             GHI
1/1/2012    1000.00
2/1/2012                    500.00
2/1/2012                                    800.00
2/10/2012                   700.00
3/1/2012    1100.00

空白点可以是 NULL 或空白,两者都可以,并且类别需要是动态的。另一个可能的警告是,我们将以有限的容量运行查询,这意味着临时表已用完。我试图研究并登陆PIVOT,但因为我从来没有使用过它,所以我真的不明白它,尽管我尽了最大的努力去弄清楚它。谁能指出我正确的方向?

【问题讨论】:

标签: sql sql-server tsql pivot


【解决方案1】:

动态 SQL PIVOT:

create table temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into temp values ('1/1/2012', 'ABC', 1000.00)
insert into temp values ('2/1/2012', 'DEF', 500.00)
insert into temp values ('2/1/2012', 'GHI', 800.00)
insert into temp values ('2/10/2012', 'DEF', 700.00)
insert into temp values ('3/1/2012', 'ABC', 1100.00)


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

SET @cols = STUFF((SELECT distinct ',' + QUOTENAME(c.category) 
            FROM temp c
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')

set @query = 'SELECT date, ' + @cols + ' from 
            (
                select date
                    , amount
                    , category
                from temp
           ) x
            pivot 
            (
                 max(amount)
                for category in (' + @cols + ')
            ) p '


execute(@query)

drop table temp

结果:

Date                        ABC         DEF    GHI
2012-01-01 00:00:00.000     1000.00     NULL    NULL
2012-02-01 00:00:00.000     NULL        500.00  800.00
2012-02-10 00:00:00.000     NULL        700.00  NULL
2012-03-01 00:00:00.000     1100.00     NULL    NULL

【讨论】:

  • 所以 \@cols 必须是字符串连接的,对吧?我们不能使用 sp_executesql 和参数绑定在其中插入 \@cols 吗?即使我们自己构建了 \@cols,如果它包含恶意 SQL 怎么办。在连接并执行它之前我可以采取任何其他缓解措施吗?
  • 你会如何对这上面的行和列进行排序?
  • @PatrickSchomburg 有多种方法 - 如果您想对 @cols 进行排序,则可以删除 DISTINCT 并在获得 @ 列表时使用 GROUP BYORDER BY 987654327@.
  • 我会试试的。行呢?我也在使用日期,但它没有按顺序出现。
  • 没关系我把订单放错地方了。
【解决方案2】:

动态 SQL PIVOT

创建列字符串的不同方法

create table #temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into #temp values ('1/1/2012', 'ABC', 1000.00)
insert into #temp values ('2/1/2012', 'DEF', 500.00)
insert into #temp values ('2/1/2012', 'GHI', 800.00)
insert into #temp values ('2/10/2012', 'DEF', 700.00)
insert into #temp values ('3/1/2012', 'ABC', 1100.00)

DECLARE @cols  AS NVARCHAR(MAX)='';
DECLARE @query AS NVARCHAR(MAX)='';

SELECT @cols = @cols + QUOTENAME(category) + ',' FROM (select distinct category from #temp ) as tmp
select @cols = substring(@cols, 0, len(@cols)) --trim "," at end

set @query = 
'SELECT * from 
(
    select date, amount, category from #temp
) src
pivot 
(
    max(amount) for category in (' + @cols + ')
) piv'

execute(@query)
drop table #temp

结果

date                    ABC     DEF     GHI
2012-01-01 00:00:00.000 1000.00 NULL    NULL
2012-02-01 00:00:00.000 NULL    500.00  800.00
2012-02-10 00:00:00.000 NULL    700.00  NULL
2012-03-01 00:00:00.000 1100.00 NULL    NULL

【讨论】:

  • 酷!您是否知道一种为值列名称添加前缀的方法 - 所以您有列的结果:日期、Amount_ABC、Amount_DEF、Amount_GHI?我在问,因为我想在其中加入一个附加值列,然后我需要区分金额列和附加值列。
【解决方案3】:

我知道这个问题比较老,但我一直在寻找答案,并认为我可以扩展问题的“动态”部分并可能帮助某人。

首先,我构建此解决方案是为了解决几个同事遇到的一个问题,即需要快速转换不稳定的大型数据集。

此解决方案需要创建一个存储过程,因此如果您无法满足需要,请立即停止阅读。

此过程将采用数据透视语句的关键变量,为不同的表、列名和聚合动态创建数据透视语句。静态列用作枢轴的分组依据/身份列(如果不需要,可以从代码中删除,但在枢轴语句中很常见并且是解决原始问题所必需的),枢轴列是最终结果列名将从中生成,值列是聚合将应用于的内容。 Table 参数是包含模式 (schema.tablename) 的表的名称,这部分代码可以使用一些爱,因为它不像我希望的那样干净。它对我有用,因为我的使用不是公开的,而且 sql 注入不是问题。 Aggregate 参数将接受任何标准 sql 聚合 'AVG'、'SUM'、'MAX' 等。代码也默认为 MAX 作为聚合,这不是必需的,但最初构建的受众不了解枢轴,通常使用 max 作为聚合。

让我们从创建存储过程的代码开始。此代码应该适用于所有版本的 SSMS 2005 及更高版本,但我尚未在 2005 或 2016 年对其进行测试,但我不明白为什么它不起作用。

create PROCEDURE [dbo].[USP_DYNAMIC_PIVOT]
    (
        @STATIC_COLUMN VARCHAR(255),
        @PIVOT_COLUMN VARCHAR(255),
        @VALUE_COLUMN VARCHAR(255),
        @TABLE VARCHAR(255),
        @AGGREGATE VARCHAR(20) = null
    )

AS


BEGIN

SET NOCOUNT ON;
declare @AVAIABLE_TO_PIVOT NVARCHAR(MAX),
        @SQLSTRING NVARCHAR(MAX),
        @PIVOT_SQL_STRING NVARCHAR(MAX),
        @TEMPVARCOLUMNS NVARCHAR(MAX),
        @TABLESQL NVARCHAR(MAX)

if isnull(@AGGREGATE,'') = '' 
    begin
        SET @AGGREGATE = 'MAX'
    end


 SET @PIVOT_SQL_STRING =    'SELECT top 1 STUFF((SELECT distinct '', '' + CAST(''[''+CONVERT(VARCHAR,'+ @PIVOT_COLUMN+')+'']''  AS VARCHAR(50)) [text()]
                            FROM '+@TABLE+'
                            WHERE ISNULL('+@PIVOT_COLUMN+','''') <> ''''
                            FOR XML PATH(''''), TYPE)
                            .value(''.'',''NVARCHAR(MAX)''),1,2,'' '') as PIVOT_VALUES
                            from '+@TABLE+' ma
                            ORDER BY ' + @PIVOT_COLUMN + ''

declare @TAB AS TABLE(COL NVARCHAR(MAX) )

INSERT INTO @TAB EXEC SP_EXECUTESQL  @PIVOT_SQL_STRING, @AVAIABLE_TO_PIVOT 

SET @AVAIABLE_TO_PIVOT = (SELECT * FROM @TAB)


SET @TEMPVARCOLUMNS = (SELECT replace(@AVAIABLE_TO_PIVOT,',',' nvarchar(255) null,') + ' nvarchar(255) null')


SET @SQLSTRING = 'DECLARE @RETURN_TABLE TABLE ('+@STATIC_COLUMN+' NVARCHAR(255) NULL,'+@TEMPVARCOLUMNS+')  
                    INSERT INTO @RETURN_TABLE('+@STATIC_COLUMN+','+@AVAIABLE_TO_PIVOT+')

                    select * from (
                    SELECT ' + @STATIC_COLUMN + ' , ' + @PIVOT_COLUMN + ', ' + @VALUE_COLUMN + ' FROM '+@TABLE+' ) a

                    PIVOT
                    (
                    '+@AGGREGATE+'('+@VALUE_COLUMN+')
                    FOR '+@PIVOT_COLUMN+' IN ('+@AVAIABLE_TO_PIVOT+')
                    ) piv

                    SELECT * FROM @RETURN_TABLE'



EXEC SP_EXECUTESQL @SQLSTRING

END

接下来,我们将为示例准备好数据。我从已接受的答案中获取了数据示例,并添加了几个数据元素以用于此概念验证,以显示聚合更改的各种输出。

create table temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into temp values ('1/1/2012', 'ABC', 1000.00)
insert into temp values ('1/1/2012', 'ABC', 2000.00) -- added
insert into temp values ('2/1/2012', 'DEF', 500.00)
insert into temp values ('2/1/2012', 'DEF', 1500.00) -- added
insert into temp values ('2/1/2012', 'GHI', 800.00)
insert into temp values ('2/10/2012', 'DEF', 700.00)
insert into temp values ('2/10/2012', 'DEF', 800.00) -- addded
insert into temp values ('3/1/2012', 'ABC', 1100.00)

以下示例显示了不同的执行语句,将不同的聚合显示为一个简单的示例。我没有选择更改静态、数据透视和值列以保持示例简单。您应该可以复制并粘贴代码以自己开始弄乱它

exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','sum'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','max'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','avg'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','min'

本次执行分别返回以下数据集。

【讨论】:

  • 干得好!您能否选择 TVF 而不是存储过程。从这样的 TVF 中选择会很方便。
  • 不幸的是,据我所知,没有,因为 TVF 不能具有动态结构。您必须在 TVF 中有一组静态列。
【解决方案4】:

使用 STRING_AGG 函数构造数据透视列列表的 SQL Server 2017 更新版本:

create table temp
(
    date datetime,
    category varchar(3),
    amount money
);

insert into temp values ('20120101', 'ABC', 1000.00);
insert into temp values ('20120201', 'DEF', 500.00);
insert into temp values ('20120201', 'GHI', 800.00);
insert into temp values ('20120210', 'DEF', 700.00);
insert into temp values ('20120301', 'ABC', 1100.00);


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

SET @cols = (SELECT STRING_AGG(category,',') FROM (SELECT DISTINCT category FROM temp WHERE category IS NOT NULL)t);

set @query = 'SELECT date, ' + @cols + ' from 
            (
                select date
                    , amount
                    , category
                from temp
           ) x
            pivot 
            (
                 max(amount)
                for category in (' + @cols + ')
            ) p ';

execute(@query);

drop table temp;

【讨论】:

  • 不幸的是,这比@mkdave99 的回答更痛苦。首先,如果您需要在构建时对数据透视列列表进行排序,则需要记住stupid MSSQL hack of including offset 0 rows。其次,您还必须记住包含不必要的表别名t 的额外愚蠢的MSSQL hack。第三,它也比@mkdave99 的回答稍慢。
  • @IanKemp 要对行进行排序,您必须ORDER BY放在查询的外部上,无需任何修改。 ORDER BY 在子查询中,即使使用 OFFSET 0 也不会像您认为的那样做,它仅用于计算偏移量以及从子查询返回的行,而不是它们可能返回的顺序。始终使用表别名,它们使代码更具可读性,不知道为什么您认为它们没有必要。 mkdave99 的答案使用变量合并,这是不可靠的,并且会给出不正确的结果,尤其是在存在 ORDER BY 的情况下,请参阅 dba.stackexchange.com/a/132709/220697
【解决方案5】:

您可以使用动态 TSQL 来实现这一点(记得使用 QUOTENAME 以避免 SQL 注入攻击):

Pivots with Dynamic Columns in SQL Server 2005

SQL Server - Dynamic PIVOT Table - SQL Injection

必须引用 The Curse and Blessings of Dynamic SQL

【讨论】:

  • FWIW QUOTENAME 仅在您接受 @tableName 作为来自用户的参数并将其附加到像 SET @sql = 'SELECT * FROM ' + @tableName; 这样的查询时才有助于 SQL 注入攻击。您可以构建大量易受攻击的动态 SQL 字符串,而 QUOTENAME 不会为您提供帮助。
  • @davids 请参考this meta discussion。如果您删除超链接,您的答案是不完整的。
  • @Kermit,我同意显示代码更有帮助,但你是说它是必需的才能成为答案吗?如果没有这些链接,我的回复是“您可以使用动态 TSQL 来实现这一点”。选择的答案建议了相同的路线,如果还展示了如何做,还有额外的好处,这就是选择它作为答案的原因。
  • 我对所选答案(在被选中之前)投了赞成票,因为它有一个例子,可以更好地帮助新人。但是,我认为新手也应该​​阅读我提供的链接,这就是我没有删除它们的原因。
【解决方案6】:

我的解决方案是清理不必要的空值

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

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

select @maxcols = STUFF((SELECT ',MAX(' + QUOTENAME(CodigoFormaPago) + ') as ' + QUOTENAME(CodigoFormaPago)
                from PO_FormasPago
                order by CodigoFormaPago
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)')
    ,1,1,'')

set @query = 'SELECT CodigoProducto, DenominacionProducto, ' + @maxcols + '
            FROM
            (
                SELECT 
                CodigoProducto, DenominacionProducto,
                ' + @cols + ' from 
                 (
                    SELECT 
                        p.CodigoProducto as CodigoProducto,
                        p.DenominacionProducto as DenominacionProducto,
                        fpp.CantidadCuotas as CantidadCuotas,
                        fpp.IdFormaPago as IdFormaPago,
                        fp.CodigoFormaPago as CodigoFormaPago
                    FROM
                        PR_Producto p
                        LEFT JOIN PR_FormasPagoProducto fpp
                            ON fpp.IdProducto = p.IdProducto
                        LEFT JOIN PO_FormasPago fp
                            ON fpp.IdFormaPago = fp.IdFormaPago
                ) xp
                pivot 
                (
                    MAX(CantidadCuotas)
                    for CodigoFormaPago in (' + @cols + ')
                ) p 
            )  xx 
            GROUP BY CodigoProducto, DenominacionProducto'

t @query;

execute(@query);

【讨论】:

    【解决方案7】:

    以下代码提供了将输出中的 NULL 替换为 zero 的结果。

    表创建和数据插入:

    create table test_table
     (
     date nvarchar(10),
     category char(3),
     amount money
     )
    
     insert into test_table values ('1/1/2012','ABC',1000.00)
     insert into test_table values ('2/1/2012','DEF',500.00)
     insert into test_table values ('2/1/2012','GHI',800.00)
     insert into test_table values ('2/10/2012','DEF',700.00)
     insert into test_table values ('3/1/2012','ABC',1100.00)
    

    查询生成确切的结果,也用零替换 NULL:

    DECLARE @DynamicPivotQuery AS NVARCHAR(MAX),
    @PivotColumnNames AS NVARCHAR(MAX),
    @PivotSelectColumnNames AS NVARCHAR(MAX)
    
    --Get distinct values of the PIVOT Column
    SELECT @PivotColumnNames= ISNULL(@PivotColumnNames + ',','')
    + QUOTENAME(category)
    FROM (SELECT DISTINCT category FROM test_table) AS cat
    
    --Get distinct values of the PIVOT Column with isnull
    SELECT @PivotSelectColumnNames 
    = ISNULL(@PivotSelectColumnNames + ',','')
    + 'ISNULL(' + QUOTENAME(category) + ', 0) AS '
    + QUOTENAME(category)
    FROM (SELECT DISTINCT category FROM test_table) AS cat
    
    --Prepare the PIVOT query using the dynamic 
    SET @DynamicPivotQuery = 
    N'SELECT date, ' + @PivotSelectColumnNames + '
    FROM test_table
    pivot(sum(amount) for category in (' + @PivotColumnNames + ')) as pvt';
    
    --Execute the Dynamic Pivot Query
    EXEC sp_executesql @DynamicPivotQuery
    

    输出:

    【讨论】:

      【解决方案8】:
      CREATE TABLE #PivotExample(
         [ID]      [nvarchar](50)   NULL,       
         [Description]   [nvarchar](50)   NULL,
         [ClientId]   [smallint] NOT NULL,
      )
      GO
      
      
      INSERT   #PivotExample  ([ID],[Description],   [ClientId])   VALUES ('ACI1','ACI1Desc1',1008)
      INSERT   #PivotExample  ([ID],[Description],   [ClientId])   VALUES ('ACI1','ACI1Desc2',2000)
      INSERT   #PivotExample  ([ID],[Description],   [ClientId])   VALUES ('ACI1','ACI1Desc3',3000)
      INSERT   #PivotExample  ([ID],[Description],   [ClientId])   VALUES ('ACI1','ACI1Desc4',4000)
      INSERT   #PivotExample  ([ID],[Description],   [ClientId])   VALUES ('ACI2','ACI2Desc1',5000)
      INSERT   #PivotExample  ([ID],[Description],   [ClientId])   VALUES ('ACI2','ACI2Desc2',6000)
      INSERT   #PivotExample  ([ID],[Description],   [ClientId])   VALUES ('ACI2','ACI2Desc3', 7000)
      
      SELECT * FROM #PivotExample
      
      
      
      
      --Declare necessary variables
      DECLARE   @SQLQuery AS NVARCHAR(MAX)
      DECLARE   @PivotColumns AS NVARCHAR(MAX)
       
      --Get unique values of pivot column  
      SELECT   @PivotColumns= COALESCE(@PivotColumns + ',','') + QUOTENAME([Description])
      FROM (SELECT DISTINCT [Description] FROM [dbo].#PivotExample) AS PivotExample
       
      --SELECT   @PivotColumns
       
      --Create the dynamic query with all the values for 
      --pivot column at runtime
      SET   @SQLQuery = 
          N' -- Your pivoted result comes here
          SELECT ID, ' + @PivotColumns + '
          FROM 
          (
              -- Source table should in a inner query
              SELECT ID,[Description],[ClientId]
              FROM #PivotExample
          )AS P
          PIVOT
          (     
                -- Select the values from derived table P
                SUM(ClientId) 
                FOR [Description] IN (' + @PivotColumns + ') 
          )AS PVTTable'
       
      --SELECT   @SQLQuery
      --Execute dynamic query
      EXEC sp_executesql @SQLQuery
      
      
      Drop table #PivotExample
      

      【讨论】:

        猜你喜欢
        相关资源
        最近更新 更多