如何使用列名称来旋转或“合并”行?

时间:2015-10-14 09:50:33

标签: sql postgresql pivot

我有下表:

crit_id   | criterium  | val1  | val2
----------+------------+-------+--------
  1       |     T01    |   9   |   9
  2       |     T02    |   3   |   5
  3       |     T03    |   4   |   9
  4       |     T01    |   2   |   3
  5       |     T02    |   5   |   1
  6       |     T03    |   6   |   1

我需要将'criterium'中的值转换为具有val1和val2的'cross product'列。所以结果必须像:

T01_val1 |T01_val2 |T02_val1 |T02_val2 | T03_val1 | T03_val2
---------+---------+---------+---------+----------+---------
   9     |   9     |    3    |    5    |    4     |    9
   2     |   3     |    5    |    1    |    6     |    1

或者换句话说:我需要将所有标准的每个值都放在一行中。

这是我目前的做法:

select 
   case when criterium = 'T01' then val1 else null end as T01_val1,
   case when criterium = 'T01' then val2 else null end as T01_val2,
   case when criterium = 'T02' then val1 else null end as T02_val1,
   case when criterium = 'T02' then val2 else null end as T02_val2,
   case when criterium = 'T03' then val1 else null end as T03_val1,
   case when criterium = 'T03' then val2 else null end as T04_val2,
from crit_table;

但结果并不是我希望它看起来像:

T01_val1 |T01_val2 |T02_val1 |T02_val2 | T03_val1 | T03_val2
---------+---------+---------+---------+----------+---------
   9     |   9     |  null   |  null   |  null    |  null
   null  |  null   |    3    |    5    |  null    |  null
   null  |  null   |  null   |  null   |   4      |    9

实现目标的最快方法是什么?

奖金问题:

我对每个标准都有77个标准和7种不同的值。所以我必须写539 case个语句。什么是动态创建它们的最佳方式?

我正在使用PostgreSql 9.4

3 个答案:

答案 0 :(得分:3)

准备交叉表

为了使用crosstab()功能,必须重新组织数据。您需要一个包含三列(row numbercriteriumvalue)的数据集。要将所有值放在一列中,您必须取消忽略最后两列,同时更改条件名称。作为行号,您可以按新条件在分区上使用rank()功能。

select rank() over (partition by criterium order by crit_id), criterium, val
from (
    select crit_id, criterium || '_v1' criterium, val1 val
    from crit
    union
    select crit_id, criterium || '_v2' criterium, val2 val
    from crit
    ) sub
order by 1, 2

 rank | criterium | val 
------+-----------+-----
    1 | T01_v1    |   9
    1 | T01_v2    |   9
    1 | T02_v1    |   3
    1 | T02_v2    |   5
    1 | T03_v1    |   4
    1 | T03_v2    |   9
    2 | T01_v1    |   2
    2 | T01_v2    |   3
    2 | T02_v1    |   5
    2 | T02_v2    |   1
    2 | T03_v1    |   6
    2 | T03_v2    |   1
(12 rows)

此数据集可用于crosstab()

create extension if not exists tablefunc;

select * from crosstab($ct$
    select rank() over (partition by criterium order by crit_id), criterium, val
    from (
        select crit_id, criterium || '_v1' criterium, val1 val
        from crit
        union
        select crit_id, criterium || '_v2' criterium, val2 val
        from crit
        ) sub
    order by 1, 2
    $ct$)
as ct (rank bigint, "T01_v1" int, "T01_v2" int, 
                    "T02_v1" int, "T02_v2" int, 
                    "T03_v1" int, "T03_v2" int);

 rank | T01_v1 | T01_v2 | T02_v1 | T02_v2 | T03_v1 | T03_v2
------+--------+--------+--------+--------+--------+--------
    1 |      9 |      9 |      3 |      5 |      4 |      9
    2 |      2 |      3 |      5 |      1 |      6 |      1
(2 rows)

替代解决方案

对于77个标准* 7参数,上述查询可能很麻烦。如果您可以接受一种不同的方式来呈现数据,那么问题就变得容易了。

select * from crosstab($ct$
    select 
        rank() over (partition by criterium order by crit_id),
        criterium,
        concat_ws(' | ', val1, val2) vals
    from crit
    order by 1, 2
    $ct$)
as ct (rank bigint, "T01" text, "T02" text, "T03" text);

 rank |  T01  |  T02  |  T03
------+-------+-------+-------
    1 | 9 | 9 | 3 | 5 | 4 | 9
    2 | 2 | 3 | 5 | 1 | 6 | 1
(2 rows)    

答案 1 :(得分:0)

DECLARE @Table1  TABLE 
    (crit_id int, criterium varchar(3), val1 int, val2 int)
;

INSERT INTO @Table1
    (crit_id, criterium, val1, val2)
VALUES
    (1, 'T01', 9, 9),
    (2, 'T02', 3, 5),
    (3, 'T03', 4, 9),
    (4, 'T01', 2, 3),
    (5, 'T02', 5, 1),
    (6, 'T03', 6, 1)
;

select [T01] As [T01_val1 ],[T01-1] As [T01_val2 ],[T02] As [T02_val1 ],[T02-1] As [T02_val2 ],[T03] As [T03_val1 ],[T03-1] As [T03_val3 ] from (
select T.criterium,T.val1,ROW_NUMBER()OVER(PARTITION BY T.criterium ORDER BY (SELECT NULL)) RN from (
select  criterium, val1 from @Table1
UNION ALL
select criterium+'-'+'1', val2 from @Table1)T)PP

PIVOT (MAX(val1) FOR criterium IN([T01],[T02],[T03],[T01-1],[T02-1],[T03-1]))P

答案 2 :(得分:0)

我同意迈克尔的评论,这个要求看起来有点奇怪,但如果你真的需要这样,那么你的解决方案是正确的。它只需要一些额外的代码(以及在val_1和val_2混合的地方进行小的修正):

select 
   sum(case when criterium = 'T01' then val_1 else null end) as T01_val1,
   sum(case when criterium = 'T01' then val_2 else null end) as T01_val2,
   sum(case when criterium = 'T02' then val_1 else null end) as T02_val1,
   sum(case when criterium = 'T02' then val_2 else null end) as T02_val2,
   sum(case when criterium = 'T03' then val_1 else null end) as T03_val1,
   sum(case when criterium = 'T03' then val_2 else null end) as T03_val2
from 
   crit_table
group by 
   trunc((crit_id-1)/3.0)
order by 
  trunc((crit_id-1)/3.0);

其工作原理如下。要将您发布的结果汇总到您想要的结果中,第一个有用的观察结果是所需的结果比初步结果的行少。因此,必须进行某种分组,关键问题是:"分组标准是什么?"在这种情况下,它是相当不明显的:它的标准ID(减1,以0开始计数)除以3,并被截断。这三个来自不同标准的数量。解决了这个难题后,很容易看出,对于聚合到同一结果行的输入行中,每列只有一个非空值。这意味着聚合函数的选择并不那么重要,因为只需要返回唯一的非空值。我在代码片段中使用了总和,但您也可以使用min或max。

至于红利问题:使用生成所需查询的代码生成器查询。代码看起来像这样(只有三种类型的值来保持简短):

with value_table as  /* possible kinds of values, add the remaining ones here */
  (select 'val_1' value_type union 
   select 'val_2' value_type union  
   select 'val_3' value_type )
select contents from (
      select 0 order_id, 'select' contents
      union
          select row_number() over () order_id, 
                 'max(case when criterium = '''||criterium||''' then '||value_type||' else null end) '||criterium||'_'||value_type||',' contents
            from crit_table
      cross join value_table
      union select 9999999 order_id, 
                   '    from crit_table  group by trunc((crit_id-1)/3.0) order by  trunc((crit_id-1)/3.0);' contents
      ) v
order by order_id;

这基本上只使用查询的字符串模板,然后为条件和val列插入适当的值组合。您甚至可以通过读取information_schema.columns中的列名来摆脱with子句,但我认为基本思想在上面的版本中更清晰。请注意,生成的代码在最后一列(在from子句之前)之后直接包含一个逗号。之后手动删除它比在生成器中更正它更容易。