如果一列不为空,则将多列转换为一行

If a column is not null then convert into a row for multiple columns

我有一个 table 如下所示,我希望将 col4 col5 和 col6 转置为行,但采用下面列出的特定模式

col1 col2 col3 col4 col5 col6
a b c 500 200
w x y 1000 300
z g h 200 600

我想把它转换成下面的样子

col1 col2 col3 col4 col5 col6
a b c 500
a b c 200
w x y 1000
w x y 300
z g h 200
z g h 600

我正在使用 unpivot 进行尝试,但无法获得所需的结果

基本上,如果在其中一列中找到空值,例如 col4 中的第一条记录,那么 SQL 查询应该忽略具有空值的 col4,但将 a b c col5 (500) 转置为一行加上 a b c col6 (200) 到另一行

您可以使用apply然后过滤掉所有-NULL值:

select t.col1, t.col2, t.col3, v.*
from t cross apply
     (values (col4, null, null), (null, col5, null), (null, null, col6)
     ) v(col4, col5, col6)
where v.col4 is not null or v.col5 is not null or v.col6 is not null;

CROSS APPLY 结合 UNION ALL 在这里非常有用:

SELECT
    t.col1, t.col2, t.col3,
    v.*
FROM table t
CROSS APPLY (
    SELECT col4, NULL, NULL
    WHERE col4 IS NOT NULL
    UNION ALL
    SELECT NULL, col5, NULL
    WHERE col5 IS NOT NULL
    UNION ALL
    SELECT NULL, NULL, col6
    WHERE col6 IS NOT NULL
) v

如果您有很多列,这会很乏味。此外,这种 table 设计通常是不正确的。你需要的是一个简单的 UNPIVOT:

SELECT
    upvt.col1,
    upvt.col2,
    upvt.col3,
    upvt.ColName,
    upvt.Value
FROM table t
UNPIVOT ( Value FOR ColName IN
    (Col4, Col5, Col6, Col7, Col8, Col9)
) upvt

通常有两种方法(好吧,三种):

  1. 使用大量输入来创建涵盖所有列的声明。这很丑但很快。最大的缺点:以后加一列会强制你重新查询。
  2. 使用适用于任意数量列的通用方法。最大的缺点:这个不会很快。
  3. 动态SQL:使用元数据创建建议1.动态的陈述。最大的缺点:这在 ad-hoc/inline 查询中永远不起作用。

为了向您展示一种通用方法,您可以对此进行测试:

DECLARE @tbl TABLE(col1 VARCHAR(10),col2 VARCHAR(10),col3 VARCHAR(10),col4 INT,col5 INT,col6 INT);
INSERT INTO @tbl VALUES
 ('a','b','c',NULL,500,200)
,('w','x','y',1000,300,NULL)    
,('z','g','h',200,NULL,600);

--通用逆透视查询

SELECT t.col1,col2,col3
        ,ROW_NUMBER() OVER(PARTITION BY col1,col2,col3 ORDER BY B.attr) AS GroupIndex
        ,B.attr.value('local-name(.)','nvarchar(max)') ColumnName
        ,B.attr.value('.','int') ColumnValue
FROM @tbl t
CROSS APPLY(SELECT(SELECT t.* FOR XML RAW,TYPE)
            .query('<cols>{/row/@*[not(local-name()=("col1","col2","col3"))]}</cols>')) A(x)
CROSS APPLY A.x.nodes('/cols/@*') B(attr);

--我们可以在 PIVOT 查询中使用

SELECT p.*
FROM
(
    SELECT t.col1,col2,col3
          ,ROW_NUMBER() OVER(PARTITION BY col1,col2,col3 ORDER BY B.attr) AS GroupIndex
          ,B.attr.value('local-name(.)','nvarchar(max)') ColumnName
          ,B.attr.value('.','int') ColumnValue
    FROM @tbl t
    CROSS APPLY(SELECT(SELECT t.* FOR XML RAW,TYPE)
                .query('<cols>{/row/@*[not(local-name()=("col1","col2","col3"))]}</cols>')) A(x)
    CROSS APPLY A.x.nodes('/cols/@*') B(attr)
) intermediateResult
PIVOT
(
    MAX(ColumnValue) FOR ColumnName IN(col4,col5,col6)
)p;

简而言之:

  • 我们使用 APPLY 创建中间 XML 列表示 col1、col2 和 col3 之后。为了实现这一点,我们首先创建一个XML,并使用query()将return除col1,col2,col3之外的所有列作为每行中的属性。
  • 我们使用 XML 的默认值 省略 NULL 值
  • 我们将其作为列 A.x 添加到结果集中。
  • 另一个APPLY将调用XML方法.nodes()。这将为每个现有属性添加一行。
  • intermediateResult 是一个 未旋转的 集合。 这是您实际应该使用的存储格式...
  • 现在我们可以使用 PIVOT 来获得想要的输出。

最大的优点是,您可以添加 col7 而无需更改它(在通用部分)。 PIVOT 的输出列表将明确需要任何新列。