如何将行数据查询为列?

Posted

技术标签:

【中文标题】如何将行数据查询为列?【英文标题】:How can I query row data as columns? 【发布时间】:2012-08-26 14:34:57 【问题描述】:

我确定我在这里遗漏了一些东西。

我有一个这样的数据集:

FK RowNumber 值类型状态 1 1 aaaaa A 新 1 2 bbbbb B 好 1 3 ccccc A 坏 1 4 ddddd C 好 1 5 eeeee B 好 2 1 fffff C 坏 2 2 ggggg 一个新的 2 3 hhhhh C 不好 3 1 iiiiii 好 3 2 jjjjj 好

我想查询前 3 个结果并将它们作为列进行透视,因此最终结果集如下所示:

FK 值 1 类型 1 状态 1 值 2 类型 2 状态 2 值 3 类型 3 状态 3 1 aaaaa A 新 bbbbb B 好 ccccc A 坏 2 fffff C 坏 ggggg A 新 hhhhh C 坏 3 iiiiii 好 jjjjj 好

如何在 SQL Server 2005 中完成此操作?

我一直在尝试使用 PIVOT 进行此操作,但我仍然对该关键字非常不熟悉,无法让它按照我想要的方式工作。

SELECT * --Id, [1], [2], [3]
FROM
(
    SELECT Id, Value, Type, Status
    , ROW_NUMBER() OVER (PARTITION BY Id ORDER Status, Type) as [RowNumber]
    FROM MyTable
) as T
PIVOT
(
    -- I know this section doesn't work. I'm still trying to figure out PIVOT
    MAX(T.Value) FOR RowNumber IN ([1], [2], [3]),
    MAX(T.Type) FOR RowNumber IN ([1], [2], [3]),
    MAX(T.Status) FOR RowNumber IN ([1], [2], [3])
) AS PivotTable;

我的实际数据集比这复杂一点,我需要前 10 条记录,而不是前 3 条,所以我不想简单地为每个记录做CASE WHEN RowNumber = X THEN...

更新

我测试了下面的所有答案,发现它们中的大多数看起来都差不多,在较小的数据集(大约 3k 条记录)中没有明显的性能差异,但是在对较大的数据集运行查询时略有不同。

这是我使用 80,000 条记录并查询前 10 行中的 5 列的测试结果,所以我的最终结果集是 50 列 + Id 列。我建议您自行测试它们,以确定哪一种最适合您和您的环境。

bluefoot's answer 的取消透视和重新透视数据的平均最快时间约为 12 秒。我也喜欢这个答案,因为我发现它最容易阅读和维护。

Aaron's answer 和 koderoid's answer 都建议使用 MAX(CASE WHEN RowNumber = X THEN ...),并且在 13 秒左右的平均时间之后紧随其后。

Rodney's answer 使用多个 PIVOT 语句的平均时间约为 16 秒,尽管使用较少的 PIVOT 语句可能会更快(我的测试有 5 个)。

建议使用 CTE 的 Aaron's answer 的前半部分和 OUTER APPLY 是最慢的。我不知道运行需要多长时间,因为我在 2 分钟后取消了它,并且大约有 3k 条记录、3 行和 3 列而不是 80k 条记录、10 行和 5 列。

【问题讨论】:

dddddeeeee 的行发生了什么? @njk 它们不包含在最终结果集中,因为我只对获取前 X 条记录感兴趣(在我的示例中,我使用的是 3,但在我的实际查询中我需要前 10 条记录) 选择“前10”的标准是什么? @njk 最终结果集需要 10 行数据。第一个数据集其实是使用ROW_NUMBER()函数得到的,可以很方便的排序过滤得到前10的记录。我更关心获取第二个结果集,它查询每一列的行数据 我认为第二组中的 Id 基本上是第一组中的 RowNumber,与第一组中的 Id 无关?第一组的 id 成为第二组数组的索引器?这应该是一个非常标准的 PIVOT。你能给出你用 PIVOT 尝试过的代码吗? 【参考方案1】:

您可以对数据进行UNPIVOTPIVOT。这可以静态或动态完成:

静态版本:

select *
from
(
  select fk, col + cast(rownumber as varchar(1)) new_col,
    val
  from 
  (
    select fk, rownumber, value, cast(type as varchar(10)) type,
      status
    from yourtable
  ) x
  unpivot
  (
    val
    for col in (value, type, status)
  ) u
) x1
pivot
(
  max(val)
  for new_col in
    ([value1], [type1], [status1], 
     [value2], [type2], [status2],
    [value3], [type3])
) p

见SQL Fiddle with demo

动态版本,这将在运行时将列列表获取到unpivot,然后到pivot

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

select @colsUnpivot = stuff((select ','+quotename(C.name)
         from sys.columns as C
         where C.object_id = object_id('yourtable') and
               C.name not in ('fk', 'rownumber')
         for xml path('')), 1, 1, '')

select @colsPivot = STUFF((SELECT  ',' 
                      + quotename(c.name 
                         + cast(t.rownumber as varchar(10)))
                    from yourtable t
                     cross apply 
                      sys.columns as C
                   where C.object_id = object_id('yourtable') and
                         C.name not in ('fk', 'rownumber')
                   group by c.name, t.rownumber
                   order by t.rownumber
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')


set @query 
  = 'select *
      from
      (
        select fk, col + cast(rownumber as varchar(10)) new_col,
          val
        from 
        (
          select fk, rownumber, value, cast(type as varchar(10)) type,
            status
          from yourtable
        ) x
        unpivot
        (
          val
          for col in ('+ @colsunpivot +')
        ) u
      ) x1
      pivot
      (
        max(val)
        for new_col in
          ('+ @colspivot +')
      ) p'

exec(@query)

见SQL Fiddle with Demo

两者都会产生相同的结果,但是如果您不提前知道列数,动态效果会很好。

动态版本在行号已经是数据集的一部分的假设下工作。

【讨论】:

+1 但VARCHAR(1) 会破坏10 的值。此外,我收到关于类型冲突的错误(您拥有的演示将 rownumber 作为表中的一列;我认为它需要在运行时制造)。所以我很难在你的演示之外进行测试。 @AaronBertrand 我以varchar(1) 为例,因为只有 1-5 个行号,因此需要将其扩展为更长的值。我的演示基于提供的数据,如果rownumber 不存在,那么是的,它需要在使用前进行评估。我基于提供的数据集显示它可以通过unpivot 然后pivot 在这里和堆上她解释说她需要 10 套(她在示例中只使用了 3 套)。 好的,我正在尝试让动态版本也能正常工作。我只是想我会立即发布一个静态版本。 :) +1 - 不错。我想知道你怎么还没回答;-)【参考方案2】:

您可以尝试在三个单独的数据透视语句中进行数据透视。请试一试:

SELECT Id
    ,MAX(S1) [Status 1]
    ,MAX(T1) [Type1]
    ,MAX(V1) [Value1]
    --, Add other columns
FROM
(
    SELECT Id, Value , Type, Status
    , 'S' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY Status, Type) AS VARCHAR(10)) [Status_RowNumber]
    , 'T' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY Status, Type) AS VARCHAR(10)) [Type_RowNumber]
    , 'V' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY Status, Type) AS VARCHAR(10)) [Value_RowNumber]
    FROM MyTable
) as T
PIVOT
(   
    MAX(Status) FOR Status_RowNumber IN ([S1], [S2], [S3],[S4],[S5],[S6],[S7],[S8],[S9],[S10])
)AS StatusPivot
PIVOT(
    MAX(Type) FOR Type_RowNumber IN ([T1], [T2], [T3],[T4],[T5],[T6],[T7],[T8],[T9],[T10])
)AS Type_Pivot
PIVOT(
    MAX(Value) FOR Value_RowNumber IN ([V1], [V2], [V3],[V4],[V5],[V6],[V7],[V8],[V9],[V10])
)AS Value_Pivot
GROUP BY Id

我不知道选择前十名记录的标准的全部范围,但这会产生和输出可能会让您更接近您的答案。

SQL Fiddle Example

【讨论】:

+1,非常聪明。但请不要无长度使用VARCHAR。 sqlblog.com/blogs/aaron_bertrand/archive/2009/10/09/… 我已更新我的声明以包含 varchar 长度。那是一篇很有帮助的文章。 您可以通过使用第二个子查询派生 row_number 来使其更加整洁。在任何一种情况下都应该只评估一次,但它看起来会更漂亮。 :-) 这实际上比我想象的要好得多!我用它来获取前 10 条记录中的 4 列,大约 4000 条 FK 记录,运行查询没有明显延迟。我仍然想尝试针对更大的结果集运行它并尝试此处发布的其他一些答案,看看是否有任何性能差异,但我很高兴实际上有一个解决方案:)(PS @AaronBertrand What你的意思是关于第二个子查询?我的查询绝对可以使用它可以获得的任何美化) @AaronBertrand Ahhhh 现在说得通了,谢谢。我得走了,但我计划在星期二把它拿回来,然后告诉你进展如何。谢谢你:)【参考方案3】:

Rodney 的多轴旋转很聪明,这是肯定的。当您进入 10X 与 3X 领域时,这里还有另外两个选择当然不那么吸引人。

;WITH a AS
(
    SELECT Id, Value, Type, Status, 
      n = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Status], [Type])
    FROM dbo.MyTable
)
SELECT a.Id, 
 Value1 = a.Value, Type1 = a.[Type], Status1 = a.[Status],
 Value2 = b.Value, Type2 = b.[Type], Status2 = b.[Status],
 Value3 = c.Value, Type3 = c.[Type], Status3 = c.[Status]
FROM a
OUTER APPLY (SELECT * FROM a AS T2 WHERE n = a.n + 1 AND id = a.id) AS b
OUTER APPLY (SELECT * FROM a AS T2 WHERE n = b.n + 1 AND id = b.id) AS c
WHERE a.n = 1
ORDER BY a.Id;

-- 或--

;WITH a AS
(
    SELECT Id, Value, [Type], [Status], 
      n = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Status], [Type])
    FROM dbo.MyTable
)
SELECT Id,
  Value1  = MAX(CASE WHEN n = 1 THEN Value    END),
  Type1   = MAX(CASE WHEN n = 1 THEN [Type]   END),
  Status1 = MAX(CASE WHEN n = 1 THEN [Status] END),
  Value2  = MAX(CASE WHEN n = 2 THEN Value    END),
  Type2   = MAX(CASE WHEN n = 2 THEN [Type]   END),
  Status2 = MAX(CASE WHEN n = 2 THEN [Status] END),
  Value3  = MAX(CASE WHEN n = 3 THEN Value    END),
  Type3   = MAX(CASE WHEN n = 3 THEN [Type]   END),
  Status3 = MAX(CASE WHEN n = 3 THEN [Status] END)
FROM a
GROUP BY Id
ORDER BY a.Id;

【讨论】:

在您的第一个场景中,您是否知道 CTE 是否会针对每个连接进行评估?这意味着如果我创建 10 个连接以获得前 10 条记录,CTE 会被评估 10 次吗? @Rachel 未经测试不可能知道,抱歉。太多的变量会决定 CTE 是否会被多次评估。 使用 CTE 的第一个查询由于性能原因肯定对我不起作用,但第二个查询的运行时间不错。如果你有兴趣,我用我的测试结果更新了我的问题:)【参考方案4】:

这可能对你有用,虽然它并不优雅。

select aa.FK_Id
    , isnull(max(aa.Value1), '') as Value1
    , isnull(max(aa.Type1), '') as Type1
    , isnull(max(aa.Status1), '') as Status1
    , isnull(max(aa.Value2), '') as Value2
    , isnull(max(aa.Type2), '') as Type2
    , isnull(max(aa.Status2), '') as Status2
    , isnull(max(aa.Value3), '') as Value3
    , isnull(max(aa.Type3), '') as Type3
    , isnull(max(aa.Status3), '') as Status3
from
(       
    select FK_Id
            , case when RowNumber = 1 then Value else null end as Value1
            , case when RowNumber = 1 then [Type] else null end as Type1
            , case when RowNumber = 1 then [Status] else null end as Status1
            , case when RowNumber = 2 then Value else null end as Value2
            , case when RowNumber = 2 then [Type] else null end as Type2
            , case when RowNumber = 2 then [Status] else null end as Status2
            , case when RowNumber = 3 then Value else null end as Value3
            , case when RowNumber = 3 then [Type] else null end as Type3
            , case when RowNumber = 3 then [Status] else null end as Status3
    from Table1
) aa
group by aa.FK_Id

【讨论】:

是的,这是我的备用计划。我需要从前 10 条记录中选择 5 列,这会产生 50 条案例陈述 当您拥有两种解决方案时,请随时向我们发布有关性能的信息。谢谢:) 非常感谢瑞秋。很高兴知道。 :-)【参考方案5】:

试试这样的:

declare @rowCount int 
set @rowCount = 10

declare @isNullClause varchar(4024)
set @isnullClause = ''
declare @caseClause varchar(4024)
set @caseClause = ''

declare @i int 
set @i = 1

while(@i <= @rowCount) begin 
    set @isnullClause = @isNullClause + 
                        ' , max(aa.Value' + CAST(@i as varchar(3)) + ') as Value'    + CAST(@i as varchar(3)) +
                        ' , max(aa.Type' + CAST(@i as varchar(3)) + ') as Type'  + CAST(@i as varchar(3)) +
                        ' , max(aa.Status' + CAST(@i as varchar(3)) + ') as Status'  + CAST(@i as varchar(3)) + ' '; 
    set @caseClause = @caseClause + 
        ' , case when RowNumber = ' + CAST(@i as varchar(3)) + ' then Value else null end as Value' + CAST(@i as varchar(3)) +
        ' , case when RowNumber = ' + CAST(@i as varchar(3)) + ' then Type else null end as Type' + CAST(@i as varchar(3)) +
        ' , case when RowNumber = ' + CAST(@i as varchar(3)) + ' then Status else null end as Status' + CAST(@i as varchar(3)) + ' '


    set @i = @i + 1; 
end

declare @sql nvarchar(4000)
set @sql = 'select aa.FK_Id ' + @isnullClause + ' from ( select FK_Id ' 
            + @caseClause + '  from Table1) aa group by aa.FK_Id '

exec SP_EXECUTESQL @sql

【讨论】:

+1 用于提供构建 CASE WHEN 版本查询的动态方式 :)

以上是关于如何将行数据查询为列?的主要内容,如果未能解决你的问题,请参考以下文章

如何将行显示为列?

如何根据日期列将行转换为列?

如何使用 Postgresql 将行转换为列?

Oracle SQL Developer:如何使用 PIVOT 函数将行转置为列

将行数据转换为列

MS Access SQL 将行转换为列