【问题标题】:SQL Server - Complex Dynamic Pivot columnsSQL Server - 复杂的动态数据透视列
【发布时间】:2014-06-12 14:08:37
【问题描述】:

我有两个表“Controls”和“ControlChilds”

父表结构:

Create table Controls(
    ProjectID Varchar(20) NOT NULL,
    ControlID INT NOT NULL,
    ControlCode Varchar(2) NOT NULL,
    ControlPoint Decimal NULL,
    ControlScore Decimal NULL,
    ControlValue Varchar(50)
)

样本数据

ProjectID | ControlID | ControlCode | ControlPoint | ControlScore | ControlValue
P001        1           A            30.44            65           Invalid
P001        2           C            45.30            85           Valid

子表结构:

Create table ControlChilds(
    ControlID INT NOT NULL,
    ControlChildID INT NOT NULL,
    ControlChildValue Varchar(200) NULL 
)

样本数据

ControlID | ControlChildID | ControlChildValue
1           100              Yes
1           101              No
1           102              NA  
1           103              Others 
2           104              Yes
2           105              SomeValue

对于给定的 ProjectID,输出应位于单行中,其所有 Control 值在前,然后是子控件值(基于 ControlCode(即)ControlCode_Child (1, 2, 3...),它应该看起来像这个

另外,我尝试了这个 PIVOT 查询,我能够获取 ChildControls 表的值,但我不知道如何获取 Controls 表的值。

DECLARE @cols AS NVARCHAR(MAX);

DECLARE @query AS NVARCHAR(MAX);
select @cols = STUFF((SELECT 
                        distinct ',' + 
                        QUOTENAME(ControlCode + '_Child' + CAST(ROW_NUMBER() over(PARTITION BY ControlCode ORDER BY ControlChildID) AS Varchar(25)))
                      FROM Controls C
                      INNER JOIN ControlChilds CC 
                      ON C.ControlID = CC.ControlID 
                      FOR XML PATH(''), TYPE
                     ).value('.', 'NVARCHAR(MAX)') 
                        , 1, 1, '');

SELECT @query ='SELECT *
FROM
(
  SELECT   
    (ControlCode + ''_Child'' + CAST(ROW_NUMBER() over(PARTITION BY ControlCode ORDER BY ControlChildID) AS Varchar(25))) As Code,
        ControlChildValue
  FROM Controls AS C
  INNER JOIN ControlChilds AS CC ON C.ControlID = CC.ControlID
) AS t
PIVOT 
(
  MAX(ControlChildValue) 
  FOR Code IN( ' + @cols + ' )' +
' ) AS p ; ';

 execute(@query);

我得到的输出:

谁能帮助我了解如何获取每个 ControlChilds 表值前面的 Controls 表值?

【问题讨论】:

  • Controls.ProjectID 被定义为INT 但它的值是P001,哪一个是正确的?
  • 好收获!它是一个错字,应该是 varchar。
  • 您是否尝试过先将其编写为静态版本以使逻辑正确?这有时比尝试立即解决动态枢轴要容易得多。
  • @bluefeet:不。我没有尝试过,但根据一些样本我这样做了。另外,我真的很困惑如何将两个表转换为一行。

标签: sql sql-server-2008 pivot


【解决方案1】:

这里有点乱,因为您有两个不同结构的表,并且您想要旋转多个列。我将首先开始编写您的查询的静态版本以使逻辑正确,然后完成编写动态版本的过程。

由于您要旋转多个列,您需要先取消旋转Controls 表中的多个列,然后再旋转。您已将此标记为 SQL Server 2008,因此您可以使用 CROSS APPLY 取消透视列。

我建议采取以下步骤。首先,取消旋转controls 表:

select 
  ProjectId,
  col = ControlCode +'_'+col,
  val
from
(
  select 
    c.ProjectId,
    c.ControlCode,
    c.ControlPoint,
    c.ControlScore,
    c.ControlValue
  from controls c
) d
cross apply
(
  select 'ControlPoint', cast(controlpoint as varchar(10)) union all
  select 'ControlScore', cast(ControlScore as varchar(10)) union all
  select 'ControlValue', ControlValue
) c (col, val)

SQL Fiddle with Demo。这会将您的多行转换为多列,类似于:

| PROJECTID |            COL |     VAL |
|-----------|----------------|---------|
|      P001 | A_ControlPoint |   30.44 |
|      P001 | A_ControlScore |   65.00 |
|      P001 | A_ControlValue | Invalid |
|      P001 | C_ControlPoint |   45.30 |
|      P001 | C_ControlScore |   85.00 |
|      P001 | C_ControlValue |   Valid |

其次,将ControlChilds 表中的数据转换为类似的格式,但使用您的row_number() 为每个孩子分配一个序列:

select 
  projectId,
  col = ControlCode+'_'+'Child'+cast(seq as varchar(10)),
  ControlChildValue
from
(
  select c.ProjectId,
    c.ControlCode,
    cc.ControlChildValue,
    row_number() over(partition by c.ProjectId, c.ControlCode
                      order by cc.ControlChildId) seq
  from controls c
  inner join controlchilds cc
    on c.controlid = cc.controlid
) d

SQL Fiddle with Demo。这会以以下格式从该表中获取数据:

| PROJECTID |      COL | CONTROLCHILDVALUE |
|-----------|----------|-------------------|
|      P001 | A_Child1 |               Yes |
|      P001 | A_Child2 |                No |
|      P001 | A_Child3 |                NA |
|      P001 | A_Child4 |            Others |
|      P001 | C_Child1 |               Yes |
|      P001 | C_Child2 |         SomeValue |

现在,您可以轻松地在两个查询之间使用UNION ALL 并应用 PIVOT 函数:

select ProjectId,
  A_ControlPoint, A_ControlScore, A_ControlValue,
  A_Child1, A_Child2, A_Child3, A_Child4,
  C_ControlPoint, C_ControlScore, C_ControlValue,
  C_Child1, C_Child2
from
(
  select 
    ProjectId,
    col = ControlCode +'_'+col,
    val
  from
  (
    select 
      c.ProjectId,
      c.ControlCode,
      c.ControlPoint,
      c.ControlScore,
      c.ControlValue
    from controls c
  ) d
  cross apply
  (
    select 'ControlPoint', cast(controlpoint as varchar(10)) union all
    select 'ControlScore', cast(ControlScore as varchar(10)) union all
    select 'ControlValue', ControlValue
  ) c (col, val)
  union all
  select 
    projectId,
    col = ControlCode+'_'+'Child'+cast(seq as varchar(10)),
    ControlChildValue
  from
  (
    select c.ProjectId,
      c.ControlCode,
      cc.ControlChildValue,
      row_number() over(partition by c.ProjectId, c.ControlCode
                        order by cc.ControlChildId) seq
    from controls c
    inner join controlchilds cc
      on c.controlid = cc.controlid
  ) d
) src
pivot
(
  max(val)
  for col in (A_ControlPoint, A_ControlScore, A_ControlValue,
              A_Child1, A_Child2, A_Child3, A_Child4,
              C_ControlPoint, C_ControlScore, C_ControlValue,
              C_Child1, C_Child2)
) piv;

SQL Fiddle with Demo

既然你有正确的逻辑,你可以把它转换成一个动态的 SQL 版本:

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

select @cols = STUFF((SELECT ',' + QUOTENAME(col) 
                    from 
                    (
                      select ControlCode,
                        col = ControlCode +'_'+col,
                        seq, 
                        so
                      from controls
                      cross apply
                      (
                        select 'ControlPoint', 0, 0 union all
                        select 'ControlScore', 0, 1 union all
                        select 'ControlValue', 0, 2 
                      ) c (col, seq, so)
                      union all
                      select  ControlCode,
                        col = ControlCode+'_'+'Child'+cast(seq as varchar(10)),
                        seq, 
                        3
                      from
                      (
                        select ControlCode, 
                          row_number() over(partition by c.ProjectId, c.ControlCode
                                                  order by cc.ControlChildId) seq
                        from controls c
                        inner join controlchilds cc
                          on c.controlid = cc.controlid
                      ) d
                    ) src
                    group by ControlCode, seq, col, so
                    order by ControlCode, so, seq
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')


set @query = 'SELECT ProjectId, ' + @cols + ' 
            from 
            (
              select ProjectId,
                col = ControlCode +''_''+col,
                val
              from
              (
                select 
                  c.ProjectId,
                  c.ControlCode,
                  c.ControlPoint,
                  c.ControlScore,
                  c.ControlValue
                from controls c
              ) d
              cross apply
              (
                select ''ControlPoint'', cast(controlpoint as varchar(10)) union all
                select ''ControlScore'', cast(ControlScore as varchar(10)) union all
                select ''ControlValue'', ControlValue
              ) c (col, val)
              union all
              select 
                projectId,
                col = ControlCode+''_Child''+cast(seq as varchar(10)),
                ControlChildValue
              from
              (
                select c.ProjectId,
                  c.ControlCode,
                  cc.ControlChildValue,
                  row_number() over(partition by c.ProjectId, c.ControlCode
                                    order by cc.ControlChildId) seq
                from controls c
                inner join controlchilds cc
                  on c.controlid = cc.controlid
              ) d
            ) x
            pivot 
            (
                max(val)
                for col in (' + @cols + ')
            ) p '

exec sp_executesql @query;

SQL Fiddle with Demo。我编写了动态版本以使列保持您在示例中使用的顺序。这可以通过使用排序顺序类型的值来完成。

这给出了最终结果:

| PROJECTID | A_CONTROLPOINT | A_CONTROLSCORE | A_CONTROLVALUE | A_CHILD1 | A_CHILD2 | A_CHILD3 | A_CHILD4 | C_CONTROLPOINT | C_CONTROLSCORE | C_CONTROLVALUE | C_CHILD1 |  C_CHILD2 |
|-----------|----------------|----------------|----------------|----------|----------|----------|----------|----------------|----------------|----------------|----------|-----------|
|      P001 |          30.44 |          65.00 |        Invalid |      Yes |       No |       NA |   Others |          45.30 |          85.00 |          Valid |      Yes | SomeValue |

【讨论】:

  • 天才!很好的解释,你让这个复杂的看起来很简单。
  • @user972255 很高兴为您提供帮助,我必须说这个问题花了我一些时间来解决正确的方法,但是您在问题中包含了很多信息来解决它,这很有帮助。
  • 现在我需要为每个子元素显示子评论。例如,A_Child1、A_Child1Comments、A_Child2、A_Child2Comments 等... 另一个棘手的事情是我只需要在 cmets 不为空时才显示它们,否则我不应该显示该列。这可能吗?我尝试了很多东西,但结果并不准确。
  • @user972255 我建议发布一个带有要求的新问题,在 cmets 中回答太难了。
【解决方案2】:

PIVOT你的数据你首先需要UNPIVOT它的一部分,Controls表中的部分。

准备查询变成

SELECT ProjectID, ControlCode + '_' + [Field] [Field], Value
FROM   (SELECT ProjectID
             , ControlCode
             , CAST(ControlPoint AS SQL_Variant) ControlPoint
             , CAST(ControlScore AS SQL_Variant) ControlScore
             , CAST(ControlValue AS SQL_Variant) ControlValue
        FROM Controls C) D
       UNPIVOT
       (Value FOR [Field] IN (ControlPoint, ControlScore, ControlValue)) p
UNION ALL
SELECT ProjectID, ControlCode + '_Child' + RowID [Field], Value
FROM   (SELECT C.ProjectID
             , C.ControlCode
             , RowID = CAST(ROW_NUMBER() 
                 OVER (PARTITION BY CC.ControlID 
                       ORDER BY CC.ControlChildID) AS VARCHAR)
             , CAST(CC.ControlChildValue AS SQL_Variant) ControlChildValue
        FROM Controls C
             INNER JOIN ControlChilds CC ON C.ControlID = CC.ControlID) D
       UNPIVOT
       (Value FOR [Field] IN (ControlChildValue)) p

SQLFiddle demo

以格式获取数据

PROJECTID | FIELD          | VALUE
----------+----------------+----------
P001      | A_ControlPoint | 30
P001      | A_ControlScore | 65
P001      | A_ControlValue | Invalid
P001      | C_ControlPoint | 45
P001      | C_ControlScore | 85
P001      | C_ControlValue | Valid
P001      | A_Child1       | Yes
P001      | A_Child2       | No
P001      | A_Child3       | NA
P001      | A_Child4       | Others
P001      | C_Child1       | Yes
P001      | C_Child2       | SomeValue

现在可以转到PIVOT

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

WITH Q AS (
  SELECT ProjectID, ControlCode + '_' + [Field] [Field], Value
       , ControlCode
       , ID = CASE WHEN [Field] = 'ControlPoint' THEN 1
                   WHEN [Field] = 'ControlScore' THEN 2
                   WHEN [Field] = 'ControlValue' THEN 3
              END
  FROM   (SELECT ProjectID
               , ControlCode
               , CAST(ControlPoint AS SQL_Variant) ControlPoint
               , CAST(ControlScore AS SQL_Variant) ControlScore
               , CAST(ControlValue AS SQL_Variant) ControlValue
          FROM Controls C) D
         UNPIVOT
         (Value FOR [Field] IN (ControlPoint, ControlScore, ControlValue)) p
  UNION ALL
  SELECT ProjectID, ControlCode + '_Child' + RowID [Field], Value
       , ControlCode
       , ID = RowID + 3
  FROM   (SELECT C.ProjectID
               , C.ControlCode
               , RowID = CAST(ROW_NUMBER() OVER (PARTITION BY CC.ControlID ORDER BY CC.ControlChildID) AS VARCHAR)
               , CAST(CC.ControlChildValue AS SQL_Variant) ControlChildValue
          FROM Controls C
               INNER JOIN ControlChilds CC ON C.ControlID = CC.ControlID) D
         UNPIVOT
         (Value FOR [Field] IN (ControlChildValue)) p
)
SELECT @cols = STUFF((SELECT ',' + QUOTENAME([Field])
                      FROM Q
                      ORDER BY ControlCode, ID
                      FOR XML PATH(''), TYPE
                     ).value('.', 'NVARCHAR(MAX)') 
                        , 1, 1, '')

SELECT @query ='
WITH Q AS (
  SELECT ProjectID, ControlCode + ''_'' + [Field] [Field], Value
  FROM   (SELECT ProjectID
               , ControlCode
               , CAST(ControlPoint AS SQL_Variant) ControlPoint
               , CAST(ControlScore AS SQL_Variant) ControlScore
               , CAST(ControlValue AS SQL_Variant) ControlValue
          FROM Controls C) D
         UNPIVOT
         (Value FOR [Field] IN (ControlPoint, ControlScore, ControlValue)) p
  UNION ALL
  SELECT ProjectID, ControlCode + ''_Child'' + RowID [Field], Value
  FROM   (SELECT C.ProjectID
               , C.ControlCode
               , RowID = CAST(ROW_NUMBER() OVER (PARTITION BY CC.ControlID ORDER BY CC.ControlChildID) AS VARCHAR)
               , CAST(CC.ControlChildValue AS SQL_Variant) ControlChildValue
          FROM Controls C
               INNER JOIN ControlChilds CC ON C.ControlID = CC.ControlID) D
         UNPIVOT
         (Value FOR [Field] IN (ControlChildValue)) p
)
SELECT *
FROM   (SELECT ProjectID, [Field], Value FROM Q) AS t
       PIVOT 
       (MAX(Value) FOR [Field] IN( ' + @cols + ' )) AS p ;'

execute(@query);

SQLFiddle demo

要获得所需顺序的字段,必须在列字符串中强制执行特定顺序,因此在SELECT ... FOR XML中,由于没有可用的顺序列,需要创建一个新的,@的含义987654331@中的CTE

【讨论】:

  • 太棒了!这也满足了我的需要,但很抱歉我只能选择一个答案。感谢您的时间和精力。
  • @user972255 选择你更喜欢的,或者你自己会用的,或者先写的 ;-)
猜你喜欢
  • 2013-10-24
  • 1970-01-01
  • 1970-01-01
  • 2022-01-06
  • 2015-04-22
  • 1970-01-01
  • 2014-10-16
  • 2014-07-26
  • 1970-01-01
相关资源
最近更新 更多