【问题标题】:SQL Server : Columns to RowsSQL Server:列到行
【发布时间】:2013-08-04 06:36:51
【问题描述】:

寻找将列转换为行的优雅(或任何)解决方案。

这是一个示例:我有一个具有以下架构的表:

[ID] [EntityID] [Indicator1] [Indicator2] [Indicator3] ... [Indicator150]

这是我想要得到的结果:

[ID] [EntityId] [IndicatorName] [IndicatorValue]

结果值将是:

1 1 'Indicator1' 'Value of Indicator 1 for entity 1'
2 1 'Indicator2' 'Value of Indicator 2 for entity 1'
3 1 'Indicator3' 'Value of Indicator 3 for entity 1'
4 2 'Indicator1' 'Value of Indicator 1 for entity 2'

等等..

这有意义吗?您对在哪里查找以及如何在 T-SQL 中完成它有什么建议吗?

【问题讨论】:

  • 你看过Pivot/Unpivot了吗?
  • 最后采用了 bluefeet 的解决方案。优雅而实用。非常感谢大家。

标签: sql sql-server tsql unpivot


【解决方案1】:

您可以使用UNPIVOT 函数将列转换为行:

select id, entityId,
  indicatorname,
  indicatorvalue
from yourtable
unpivot
(
  indicatorvalue
  for indicatorname in (Indicator1, Indicator2, Indicator3)
) unpiv;

请注意,您要反透视的列的数据类型必须相同,因此您可能必须在应用反透视之前转换数据类型。

您也可以使用CROSS APPLY 和 UNION ALL 来转换列:

select id, entityid,
  indicatorname,
  indicatorvalue
from yourtable
cross apply
(
  select 'Indicator1', Indicator1 union all
  select 'Indicator2', Indicator2 union all
  select 'Indicator3', Indicator3 union all
  select 'Indicator4', Indicator4 
) c (indicatorname, indicatorvalue);

根据您的 SQL Server 版本,您甚至可以将 CROSS APPLY 与 VALUES 子句一起使用:

select id, entityid,
  indicatorname,
  indicatorvalue
from yourtable
cross apply
(
  values
  ('Indicator1', Indicator1),
  ('Indicator2', Indicator2),
  ('Indicator3', Indicator3),
  ('Indicator4', Indicator4)
) c (indicatorname, indicatorvalue);

最后,如果您有 150 列要取消透视,并且您不想对整个查询进行硬编码,那么您可以使用动态 SQL 生成 sql 语句:

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

select @colsUnpivot 
  = stuff((select ','+quotename(C.column_name)
           from information_schema.columns as C
           where C.table_name = 'yourtable' and
                 C.column_name like 'Indicator%'
           for xml path('')), 1, 1, '')

set @query 
  = 'select id, entityId,
        indicatorname,
        indicatorvalue
     from yourtable
     unpivot
     (
        indicatorvalue
        for indicatorname in ('+ @colsunpivot +')
     ) u'

exec sp_executesql @query;

【讨论】:

  • 对于那些想要更多关于UNPIVOT 和/vs 的螺母和螺栓的人。 APPLYthis 2010 blog post from Brad Schulz(和the follow-on)很漂亮。
  • Msg 8167, Level 16, State 1, Line 147 列“blahblah”的类型与 UNPIVOT 列表中指定的其他列的类型冲突。
  • @JDPeckham 如果您有不同的数据类型,那么您需要在执行反透视之前将它们转换为相同的类型和长度。这里是more information about that
  • xml 方法有一个缺陷,因为它无法对 >、< 等 xml 代码进行转义。和&.另外,通过如下重写可以显着提高性能: select @colsUnpivot = stuff((select ','+quotename(C.column_name) as [text()] from information_schema.columns as C where C.table_name = 'yourtable'和 C.column_name like 'Indicator%' for xml path(''), type).value('text()[1]','nvarchar(max)'), 1, 1, '')
  • 我真的很喜欢这篇文章。谢谢@Taryn。
【解决方案2】:

好吧,如果您有 150 列,那么我认为 UNPIVOT 不是一个选项。所以你可以使用 xml 技巧

;with CTE1 as (
    select ID, EntityID, (select t.* for xml raw('row'), type) as Data
    from temp1 as t
), CTE2 as (
    select
         C.id, C.EntityID,
         F.C.value('local-name(.)', 'nvarchar(128)') as IndicatorName,
         F.C.value('.', 'nvarchar(max)') as IndicatorValue
    from CTE1 as c
        outer apply c.Data.nodes('row/@*') as F(C)
)
select * from CTE2 where IndicatorName like 'Indicator%'

sql fiddle demo

您也可以编写动态 SQL,但我更喜欢 xml - 对于动态 SQL,您必须有权直接从表中选择数据,但这并不总是一种选择。

更新
由于 cmets 的火爆,我想我会添加一些 xml/动态 SQL 的优缺点。我会尽量做到客观,不提优雅和丑陋。如果您还有其他优缺点,请编辑答案或写在 cmets 中

缺点

  • 不如动态 SQL 快,粗略的测试表明 xml 比动态 SQL 慢了大约 2.5 倍(它是对 ~250000 行表的一次查询,所以这个估计并不准确) .如果需要,您可以自己比较,这里是 sqlfiddle 示例,在 100000 行上,它是 29s (xml) vs 14s (dynamic);
  • 对于不熟悉 xpath 的人来说可能更难理解

专业人士

  • 它与您的其他查询范围相同,这可能非常方便。想到几个例子
    • 您可以在 触发器 中查询 inserteddeleted 表(动态时根本不可能);
    • 用户不必拥有从表中直接选择的权限。我的意思是如果你有存储过程层并且用户有运行sp的权限,但没有直接查询表的权限,你仍然可以在存储过程中使用这个查询;
    • 您可以查询表变量您已在您的范围内填充(要将其传递到动态 SQL 中,您必须将其设为临时表或创建类型并将其作为参数传递给动态 SQL ;
  • 您可以在函数内执行此查询(标量或表值)。不能在函数内部使用动态 SQL;

【讨论】:

  • 您使用 XML 选择哪些不需要从表中选择数据的数据?
  • 例如,您可以决定不授予用户从表中选择数据的权限,而只授予使用表的存储过程,因此我可以在过程中选择 xml,但我必须使用一些解决方法如果我想使用动态 SQL
  • 如果您希望您的用户能够执行代码,您必须授予他们执行代码所需的任何访问权限。不要编造不存在的要求来使您的答案听起来更好(您也不必评论竞争答案来查看您的答案 - 如果他们找到了该答案,他们也可以找到您的答案)。跨度>
  • 另外,如果您使用 XML 的理由是您可以将其放在存储过程中以避免直接访问表,那么您的示例可能应该展示如何将其放入存储过程以及如何授予用户权限,以便他们可以在没有读取基础表的情况下执行它。对我来说,这是范围蔓延,因为大多数针对表编写查询的人都具有对该表的读取权限。
  • 我想说 10 倍的持续时间差异确实很重要,是的。大约 8,000 行并不是“大量数据”——我们是否应该看看 800,000 行会发生什么?
【解决方案3】:

为了帮助新读者,我创建了一个示例来更好地理解@bluefeet 对 UNPIVOT 的回答。

 SELECT id
        ,entityId
        ,indicatorname
        ,indicatorvalue
  FROM (VALUES
        (1, 1, 'Value of Indicator 1 for entity 1', 'Value of Indicator 2 for entity 1', 'Value of Indicator 3 for entity 1'),
        (2, 1, 'Value of Indicator 1 for entity 2', 'Value of Indicator 2 for entity 2', 'Value of Indicator 3 for entity 2'),
        (3, 1, 'Value of Indicator 1 for entity 3', 'Value of Indicator 2 for entity 3', 'Value of Indicator 3 for entity 3'),
        (4, 2, 'Value of Indicator 1 for entity 4', 'Value of Indicator 2 for entity 4', 'Value of Indicator 3 for entity 4')
       ) AS Category(ID, EntityId, Indicator1, Indicator2, Indicator3)
UNPIVOT
(
    indicatorvalue
    FOR indicatorname IN (Indicator1, Indicator2, Indicator3)
) UNPIV;

【讨论】:

  • 感谢您的帮助
【解决方案4】:

我需要一个解决方案来在 Microsoft SQL Server 中将列转换为行,而无需知道列名(在触发器中使用)并且没有动态 sql(动态 sql 太慢而无法在触发器中使用)。

我终于找到了这个解决方案,效果很好:

SELECT
    insRowTbl.PK,
    insRowTbl.Username,
    attr.insRow.value('local-name(.)', 'nvarchar(128)') as FieldName,
    attr.insRow.value('.', 'nvarchar(max)') as FieldValue 
FROM ( Select      
          i.ID as PK,
          i.LastModifiedBy as Username,
          convert(xml, (select i.* for xml raw)) as insRowCol
       FROM inserted as i
     ) as insRowTbl
CROSS APPLY insRowTbl.insRowCol.nodes('/row/@*') as attr(insRow)

如您所见,我将行转换为 XML(子查询选择 i,* for xml raw,这会将所有列转换为一个 xml 列)

然后我 CROSS APPLY 将一个函数应用于该列的每个 XML 属性,以便每个属性得到一行。

总体而言,这会将列转换为行,而无需知道列名,也无需使用动态 sql。这对我的目的来说已经足够快了。

(编辑:我刚刚在上面看到 Roman Pekar 的回答,他也在做同样的事情。 我先用了带游标的动态sql触发器,比这个方案慢10到100倍,但可能是游标引起的,而不是动态sql引起的。无论如何,这个解决方案非常简单通用,所以它绝对是一个选择)。

我在这个地方留下这个评论,因为我想在我的帖子中引用这个关于完整审计触发器的解释,你可以在这里找到:https://stackoverflow.com/a/43800286/4160788

【讨论】:

    【解决方案5】:

    只是因为我没有看到它提到。

    如果是 2016+,这里还有另一个选项可以在不实际使用动态 SQL 的情况下动态取消透视数据。

    示例

    Declare @YourTable Table ([ID] varchar(50),[Col1] varchar(50),[Col2] varchar(50))
    Insert Into @YourTable Values 
     (1,'A','B')
    ,(2,'R','C')
    ,(3,'X','D')
    
    Select A.[ID]
          ,Item  = B.[Key]
          ,Value = B.[Value]
     From  @YourTable A
     Cross Apply ( Select * 
                    From  OpenJson((Select A.* For JSON Path,Without_Array_Wrapper )) 
                    Where [Key] not in ('ID','Other','Columns','ToExclude')
                 ) B
    

    退货

    ID  Item    Value
    1   Col1    A
    1   Col2    B
    2   Col1    R
    2   Col2    C
    3   Col1    X
    3   Col2    D
    

    【讨论】:

      【解决方案6】:
      DECLARE @TableName varchar(max)=NULL
      SELECT @TableName=COALESCE(@TableName+',','')+t.TABLE_CATALOG+'.'+ t.TABLE_SCHEMA+'.'+o.Name
        FROM sysindexes AS i
        INNER JOIN sysobjects AS o ON i.id = o.id
        INNER JOIN INFORMATION_SCHEMA.TABLES T ON T.TABLE_NAME=o.name
       WHERE i.indid < 2
        AND OBJECTPROPERTY(o.id,'IsMSShipped') = 0
        AND i.rowcnt >350
        AND o.xtype !='TF'
       ORDER BY o.name ASC
      
       print @tablename
      

      您可以获得行数 >350 的表列表。您可以在表格的解决方案列表中查看行。

      【讨论】:

        【解决方案7】:

        与此相反的是将一列展平为 csv eg

        SELECT STRING_AGG ([value],',') FROM STRING_SPLIT('Akio,Hiraku,Kazuo', ',')

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2023-02-08
          • 2013-09-10
          • 1970-01-01
          • 2015-10-02
          • 2021-08-02
          相关资源
          最近更新 更多