将 1 行表展平为键值对表

Posted

技术标签:

【中文标题】将 1 行表展平为键值对表【英文标题】:Flattening of a 1 row table into a key-value pair table 【发布时间】:2011-11-12 13:23:57 【问题描述】:

获取表示一行中的列值的键值对结果集的最佳方法是什么?

鉴于下表 A 只有 1 行


Column1 Column2 Column3 ...
Value1  Value2  Value3

我想查询它并插入另一个表B:


Key                  Value
Column1              Value1
Column2              Value2
Column3              Value3

事先不知道表 A 中的一组列。

注意:我正在查看 FOR XML 和 PIVOT 功能以及动态 SQL 来执行以下操作:


    DECLARE @sql nvarchar(max)
    SET @sql = (SELECT STUFF((SELECT ',' + column_name 
                              FROM INFORMATION_SCHEMA.COLUMNS 
                              WHERE table_name='TableA' 
                              ORDER BY column_name FOR XML PATH('')), 1, 1, ''))
    SET @sql = 'SELECT ' + @sql + ' FROM TableA'
    EXEC(@sql)

【问题讨论】:

这是 UNPIVOT 不是 PIVOT,但您仍然需要动态 SQL。 马丁的评论+1。由于您不知道未透视的列,因此您需要动态 SQL。 @Aaron - 只是一个错字,现已修正 【参考方案1】:

不涉及动态的版本。如果您有无效的列名用作 XML 中的元素名,这将失败。

select T2.N.value('local-name(.)', 'nvarchar(128)') as [Key],
       T2.N.value('text()[1]', 'nvarchar(max)') as Value
from (select *
      from TableA
      for xml path(''), type) as T1(X)
  cross apply T1.X.nodes('/*') as T2(N)

一个工作样本:

declare @T table
(
  Column1 varchar(10), 
  Column2 varchar(10), 
  Column3 varchar(10)
)

insert into @T values('V1','V2','V3')

select T2.N.value('local-name(.)', 'nvarchar(128)') as [Key],
       T2.N.value('text()[1]', 'nvarchar(max)') as Value
from (select *
      from @T
      for xml path(''), type) as T1(X)
  cross apply T1.X.nodes('/*') as T2(N)

结果:

Key                  Value
-------------------- -----
Column1              V1
Column2              V2
Column3              V3

更新

对于包含多个表的查询,您可以使用for xml auto 来获取 XML 中的表名。请注意,如果您在查询中使用别名作为表名,您将获得别名。

select X2.N.value('local-name(..)', 'nvarchar(128)') as TableName,
       X2.N.value('local-name(.)', 'nvarchar(128)') as [Key],
       X2.N.value('text()[1]', 'nvarchar(max)') as Value
from (
     -- Your query starts here
     select T1.T1ID,
            T1.T1Col,
            T2.T2ID,
            T2.T2Col
     from T1
       inner join T2
         on T1.T1ID = T2.T1ID
     -- Your query ends here
     for xml auto, elements, type     
     ) as X1(X)
  cross apply X1.X.nodes('//*[text()]') as X2(N)

SQL Fiddle

【讨论】:

不得不承认,这是一个有趣的方法。 我刚刚为 FOR XML PATH(''), ELEMENTS XSINIL, TYPE) AS T1(X) 添加了 ELEMENTS XSINIL 以包含具有 NULL 的列。谢谢 我的数据集连接了多个具有相同列名的表。有没有一种简单的方法来获取包含表名的列名(无需手动为所有内容命名)?【参考方案2】:

我想你已经成功了一半。只需按照 Martin 的建议使用 UNPIVOTdynamic SQL

CREATE TABLE TableA (
  Code VARCHAR(10),
  Name VARCHAR(10),
  Details VARCHAR(10)
) 

INSERT TableA VALUES ('Foo', 'Bar', 'Baz') 
GO

DECLARE @sql nvarchar(max)
SET @sql = (SELECT STUFF((SELECT ',' + column_name 
                          FROM INFORMATION_SCHEMA.COLUMNS 
                          WHERE table_name='TableA' 
                          ORDER BY ordinal_position FOR XML PATH('')), 1, 1, ''))

SET @sql = N'SELECT [Key], Val FROM (SELECT ' + @sql + ' FROM TableA) x '
+ 'UNPIVOT ( Val FOR [Key] IN (' + @sql + ')) AS unpiv'
EXEC (@sql)

结果

Key          Val
------------ ------------
Code         Foo
Name         Bar
Details      Baz

当然,有一个警告。您的所有列都需要具有相同的数据类型才能使上述代码正常工作。如果不是,您将收到此错误:

Msg 8167, Level 16, State 1, Line 1
The type of column "Col" conflicts with the type of 
other columns specified in the UNPIVOT list.

为了解决这个问题,您需要创建两个列字符串语句。一个获取列,一个将它们全部转换为 Val 列的数据类型。

对于多种列类型

CREATE TABLE TableA (
  Code INT,
  Name VARCHAR(10),
  Details VARCHAR(10)
) 

INSERT TableA VALUES (1, 'Foo', 'Baf') 
GO

DECLARE 
  @sql nvarchar(max),
  @cols nvarchar(max),
  @conv nvarchar(max) 

SET @cols = (SELECT STUFF((SELECT ',' + column_name 
                          FROM INFORMATION_SCHEMA.COLUMNS 
                          WHERE table_name='TableA' 
                          ORDER BY ordinal_position FOR XML PATH('')), 1, 1, ''))

SET @conv = (SELECT STUFF((SELECT ', CONVERT(VARCHAR(50), ' 
                          + column_name + ') AS ' + column_name
                          FROM INFORMATION_SCHEMA.COLUMNS 
                          WHERE table_name='TableA' 
                          ORDER BY ordinal_position FOR XML PATH('')), 1, 1, ''))


SET @sql = N'SELECT [Key], Val FROM (SELECT ' + @conv + ' FROM TableA) x '
+ 'UNPIVOT ( Val FOR [Key] IN (' + @cols + ')) AS unpiv'
EXEC (@sql)

【讨论】:

【参考方案3】:

也许你让这变得比它需要的更复杂。部分原因是我无法用我的小脑袋来处理PIVOT/UNPIVOT/whatever 组合的数量,并且需要动态 SQL“红海”来实现这一目标。由于您知道该表只有一行,因此提取每一列的值可以只是作为一组 UNIONed 查询的一部分的子查询。

DECLARE @sql NVARCHAR(MAX) = N'INSERT dbo.B([Key], Value) '

SELECT @sql += CHAR(13) + CHAR(10) 
        + ' SELECT [Key] = ''' + REPLACE(name, '''', '''''') + ''', 
        Value = (SELECT ' + QUOTENAME(name) + ' FROM dbo.A) UNION ALL'
FROM sys.columns 
WHERE [object_id] = OBJECT_ID('dbo.A');

SET @sql = LEFT(@sql, LEN(@sql)-9) + ';';

PRINT @sql;
-- EXEC sp_executesql @sql;

结果(我只创建了 4 列,但这适用于任何数字):

INSERT dbo.B([Key], Value)
 SELECT [Key] = 'Column1', 
        Value = (SELECT [Column1] FROM dbo.A) UNION ALL
 SELECT [Key] = 'Column2', 
        Value = (SELECT [Column2] FROM dbo.A) UNION ALL
 SELECT [Key] = 'Column3', 
        Value = (SELECT [Column3] FROM dbo.A) UNION ALL
 SELECT [Key] = 'Column4', 
        Value = (SELECT [Column4] FROM dbo.A);

世界上最高效的东西?可能不会。但同样,对于单排表,希望是一次性任务,我认为它会工作得很好。如果您允许在您的商店中使用这些东西,请注意包含撇号的列名...

编辑抱歉,不能这样。现在它将处理列名中的撇号和其他次优命名选择。

【讨论】:

以上是关于将 1 行表展平为键值对表的主要内容,如果未能解决你的问题,请参考以下文章

将NSString转换为键值对

将 json 字典列转换为键值对行(Redshift+Postgresql)

将 json 格式的键值对转换为以符号为键的 ruby​​ 哈希的最佳方法是啥?

Pyspark 将行数据转换为键值对

使用 Apache Spark 将键值对缩减为键列表对

如何在php中将对象数组转换为键值对