使用CASE和GROUP BY进行数据透视的动态替代方法

fli*_*p99 26 sql postgresql pivot crosstab window-functions

我有一个看起来像这样的表:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D
Run Code Online (Sandbox Code Playgroud)

我希望它看起来像这样:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8
Run Code Online (Sandbox Code Playgroud)

我有这个查询,它执行此操作:

SELECT bar, 
   MAX(CASE WHEN abc."row" = 1 THEN feh ELSE NULL END) AS "val1",
   MAX(CASE WHEN abc."row" = 2 THEN feh ELSE NULL END) AS "val2",
   MAX(CASE WHEN abc."row" = 3 THEN feh ELSE NULL END) AS "val3"
FROM
(
  SELECT bar, feh, row_number() OVER (partition by bar) as row
  FROM "Foo"
 ) abc
GROUP BY bar
Run Code Online (Sandbox Code Playgroud)

如果要创建许多新列,这是一种非常狡猾的方法并且变得难以处理.我想知道这些CASE陈述是否可以更好地使这个查询更具动态性?此外,我很乐意看到其他方法来做到这一点.

Erw*_*ter 51

如果你还没有安装附加模块tablefunc,运行此命令一次,每个数据库:

CREATE EXTENSION tablefunc;
Run Code Online (Sandbox Code Playgroud)

回答问题

适合您案例的非常基本的交叉表解决方案:

SELECT * FROM crosstab(
  'SELECT bar, 1 AS cat, feh
   FROM   tbl_org
   ORDER  BY bar, feh')
 AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?
Run Code Online (Sandbox Code Playgroud)

这里的特殊困难是,基表中没有category(cat).对于基本的1参数形式,我们可以提供一个虚拟列,其虚拟值用作类别.无论如何,该值都被忽略.

这是一个罕见的情况下,其中第二个参数crosstab()的功能并不需要,因为所有的NULL值只出现在这一问题的界定,晃来晃去的列到右边.订单可以由价值决定.

如果我们有一个实际的类别与名称确定结果值的顺序列,我们需要的2参数形式crosstab().在这里,我与合成窗函数的帮助类别列row_number(),立足crosstab()于:

SELECT * FROM crosstab(
   $$
   SELECT bar, val, feh
   FROM  (
      SELECT *, 'val' || row_number() OVER (PARTITION BY bar ORDER BY feh) AS val
      FROM tbl_org
      ) x
   ORDER BY 1, 2
   $$
 , $$VALUES ('val1'), ('val2'), ('val3')$$         -- more columns?
) AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?
Run Code Online (Sandbox Code Playgroud)

其余的几乎都是普通的.在这些密切相关的答案中找到更多解释和链接.

基础知识:
如果您不熟悉该crosstab()功能,请先阅读此内容!

高级:

适当的测试设置

这就是你应该如何提供一个测试用例:

CREATE TEMP TABLE tbl_org (id int, feh int, bar text);
INSERT INTO tbl_org (id, feh, bar) VALUES
   (1, 10, 'A')
 , (2, 20, 'A')
 , (3,  3, 'B')
 , (4,  4, 'B')
 , (5,  5, 'C')
 , (6,  6, 'D')
 , (7,  7, 'D')
 , (8,  8, 'D');
Run Code Online (Sandbox Code Playgroud)

动态交叉表?

然而,正如@Clodoaldo评论的那样,还不是很有活力.使用plpgsql很难实现动态返回类型.但是有很多方法 - 有一些限制.

所以不要进一步使其余部分复杂化,我用一个更简单的测试用例来证明:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);
Run Code Online (Sandbox Code Playgroud)

呼叫:

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2')
AS ct (row_name text, val1 int, val2 int, val3 int);
Run Code Online (Sandbox Code Playgroud)

返回:

 row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8
Run Code Online (Sandbox Code Playgroud)

tablefunc模块的内置功能

tablefunc模块为通用crosstab()调用提供了一个简单的基础结构,而不提供列定义列表.写入的许多函数 C(通常非常快):

__PRE__

crosstab1()- crosstab4()是预先定义的.一个小问题:他们需要并返回所有text.所以我们需要施展我们的integer价值观.但它简化了通话:

crosstabN()
Run Code Online (Sandbox Code Playgroud)

结果:

SELECT * FROM crosstab4('SELECT row_name, attrib, val::text  -- cast!
                         FROM tbl ORDER BY 1,2')
Run Code Online (Sandbox Code Playgroud)

自定义crosstab()功能

对于更多列其他数据类型,我们创建自己的复合类型函数(一次).
类型:

 row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |
Run Code Online (Sandbox Code Playgroud)

功能:

CREATE TYPE tablefunc_crosstab_int_5 AS (
  row_name text, val1 int, val2 int, val3 int, val4 int, val5 int);
Run Code Online (Sandbox Code Playgroud)

呼叫:

CREATE OR REPLACE FUNCTION crosstab_int_5(text)
  RETURNS SETOF tablefunc_crosstab_int_5
AS '$libdir/tablefunc', 'crosstab' LANGUAGE c STABLE STRICT;
Run Code Online (Sandbox Code Playgroud)

结果:

SELECT * FROM crosstab_int_5('SELECT row_name, attrib, val   -- no cast!
                              FROM tbl ORDER BY 1,2');
Run Code Online (Sandbox Code Playgroud)

所有的一个多态,动态函数

这超出了tablefunc模块所涵盖的范围.
为了使返回类型动态,我使用多态类型,并在此相关答案中详细说明了该技术:

1参数形式:

 row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |
Run Code Online (Sandbox Code Playgroud)

使用此变量为2参数形式重载:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L) t(%s)'
                , _qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

pg_typeof(_rowtype)::text::regclass:为每个用户定义的复合类型定义了行类型,以便在系统目录中列出属性(列)pg_attribute.获得它的快车道:将注册类型(regtype)转换为text并将其转换textregclass.

一次创建复合类型:

您需要为要使用的每种返回类型定义一次:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L, %L) t(%s)'
                , _qry, _cat_qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

对于临时调用,您还可以创建一个临时表到相同(临时)效果:

CREATE TYPE tablefunc_crosstab_int_3 AS (
    row_name text, val1 int, val2 int, val3 int);

CREATE TYPE tablefunc_crosstab_int_4 AS (
    row_name text, val1 int, val2 int, val3 int, val4 int);

...
Run Code Online (Sandbox Code Playgroud)

或者使用现有表,视图或物化视图的类型(如果可用).

呼叫

使用上面的行类型:

1参数形式(无缺失值):

CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);
Run Code Online (Sandbox Code Playgroud)

2参数形式(某些值可能丢失):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1,2'
 , NULL::tablefunc_crosstab_int_3);
Run Code Online (Sandbox Code Playgroud)

这个函数适用于所有返回类型,而模块提供的框架需要为每个类型提供单独的函数. 如果按照上面的说明顺序命名了类型,则只需要替换粗体数字.要查找基表中的最大类别数:crosstabN()tablefunc

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1'
 , $$VALUES ('val1'), ('val2'), ('val3')$$
 , NULL::tablefunc_crosstab_int_3);
Run Code Online (Sandbox Code Playgroud)

如果您想要单独的列,那就像动态一样.阵列喜欢由@Clocoaldo证明或一个简单的文本表示或结果包裹在文档类型像jsonhstore可以为任何数量的类别的工作动态.

免责声明:
当用户输入转换为代码时,它总是有潜在危险.确保这不能用于SQL注入.不接受来自不受信任的用户的输入(直接).

征集原始问题:

SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;
Run Code Online (Sandbox Code Playgroud)

  • 我希望我可以投票一百次. (8认同)

Dam*_*ney 15

虽然这是一个老问题,但我希望通过PostgreSQL的最新改进添加另一个解决方案.该解决方案实现了相同的目标,即在不使用交叉表功能的情况下从动态数据集返回结构化结果. 换句话说,这是重新审视无意和隐含假设的一个很好的例子,这些假设阻止我们发现旧问题的新解决方案.;)

为了说明,您要求使用以下结构转置数据的方法:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D
Run Code Online (Sandbox Code Playgroud)

进入这种格式:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8
Run Code Online (Sandbox Code Playgroud)

传统的解决方案是创建动态交叉表查询的一种聪明(且难以置信的知识)方法,在Erwin Brandstetter的答案中详细解释了这一点.

但是,如果您的特定用例足够灵活,可以接受稍微不同的结果格式,那么另一种解决方案可以很好地处理动态枢轴.这种技术,我在这里学到了

使用PostgreSQL的新jsonb_object_agg函数以JSON对象的形式即时构建数据透视数据.

我将使用Brandstetter先生的"更简单的测试用例"来说明:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);
Run Code Online (Sandbox Code Playgroud)

使用该jsonb_object_agg功能,我们可以使用这种精湛的美感创建所需的旋转结果集:

SELECT
  row_name AS bar,
  json_object_agg(attrib, val) AS data
FROM tbl
GROUP BY row_name
ORDER BY row_name;
Run Code Online (Sandbox Code Playgroud)

哪个输出:

 bar |                  data                  
-----+----------------------------------------
 A   | { "val1" : 10, "val2" : 20 }
 B   | { "val1" : 3, "val2" : 4 }
 C   | { "val1" : 5 }
 D   | { "val3" : 8, "val1" : 6, "val2" : 7 }
Run Code Online (Sandbox Code Playgroud)

如您所见,此函数通过在样本数据中的attribvalue列中创建JSON对象中的键/值对来进行工作,所有这些对都按分组row_name.

虽然这个结果集显然看起来不同,但我相信它实际上会满足许多(如果不是大多数)现实世界的用例,特别是那些数据需要动态生成的数据透视表,或者父数据库使用结果数据的情况(例如,需要重新格式化以便在http响应中传输).

这种方法的好处:

  • 更清晰的语法. 我想每个人都会同意这种方法的语法比最基本的交叉表示例更清晰,更容易理解.

  • 完全动态. 不需要事先指定有关基础数据的信息.无论是列名还是数据类型都不需要提前知道.

  • 处理大量列. 由于透视数据保存为单个jsonb列,因此您不会遇到PostgreSQL的列限制(我相信≤1,600列).仍有一个限制,但我相信它与文本字段相同:每个JSON对象创建1 GB(如果我错了,请纠正我).这是很多关键/价值对!

  • 简化数据处理. 我相信在数据库中创建JSON数据将简化(并可能加速)父应用程序中的数据转换过程.(您将注意到我们的示例测试用例中的整数数据已正确存储在生成的JSON对象中.PostgreSQL通过根据JSON规范自动将其内部数据类型转换为JSON来处理此问题.)这将有效地消除需求手动转换传递给父应用程序的数据:它们都可以委托给应用程序的本机JSON解析器.

差异(和可能的缺点):

  • 它看起来不同. 不可否认,这种方法的结果看起来不同.JSON对象不如交叉表结果集漂亮; 然而,差异纯粹是装饰性的.生成相同的信息 - 并且格式可能适合父应用程序使用.

  • 缺少钥匙. 交叉表方法中缺少的值用空值填充,而JSON对象只是缺少适用的键.如果这是您的用例可接受的权衡,您将不得不自己决定.在我看来,任何在PostgreSQL中解决这个问题的尝试都会使这个过程变得非常复杂,并且可能会以其他查询的形式进行一些内省.

  • 密钥订单不会保留. 我不知道这是否可以在PostgreSQL中解决,但是这个问题主要是装饰性的,因为任何父应用程序要么不太可能依赖于键顺序,要么能够通过其他方式确定正确的键顺序.最坏的情况可能只需要对数据库进行额外查询.

结论

我很好奇听到其他人(尤其是@ ErwinBrandstetter)对这种方法的看法,特别是因为它与性能有关.当我在Andrew Bender的博客上发现这种方法时,就好像被击中头部一样.对PostrgeSQL中的难题采取新方法是多么美妙的方式.它完美地解决了我的用例,我相信它也会同样服务于其他许多用户.

  • 可靠的答案。好奇,现在如何按照 OP 的要求将 JSON 键转换为列? (2认同)

Clo*_*eto 6

这是完成@Damian的好答案.在9.6的便捷json_object_agg功能之前,我已经在其他答案中提出了JSON方法.使用以前的工具集只需要更多的工作.

引用的两个可能的缺点实际上并非如此.如有必要,可以轻松纠正随机密钥顺序.丢失的密钥(如果相关)需要处理几乎微不足道的代码:

select
    row_name as bar,
    json_object_agg(attrib, val order by attrib) as data
from
    tbl
    right join
    (
        (select distinct row_name from tbl) a
        cross join
        (select distinct attrib from tbl) b
    ) c using (row_name, attrib)
group by row_name
order by row_name
;
 bar |                     data                     
-----+----------------------------------------------
 a   | { "val1" : 10, "val2" : 20, "val3" : null }
 b   | { "val1" : 3, "val2" : 4, "val3" : null }
 c   | { "val1" : 5, "val2" : null, "val3" : null }
 d   | { "val1" : 6, "val2" : 7, "val3" : 8 }
Run Code Online (Sandbox Code Playgroud)

对于理解JSON的最终查询使用者而言,没有任何缺点.唯一的一个是它不能作为表源使用.

  • 有没有办法将 JSON 数据转换为包含列的表? (2认同)

Clo*_*eto 5

在你的情况下,我猜一个阵列是好的.SQL小提琴

select
    bar,
    feh || array_fill(null::int, array[c - array_length(feh, 1)]) feh
from
    (
        select bar, array_agg(feh) feh
        from foo
        group by bar
    ) s
    cross join (
        select count(*)::int c
        from foo
        group by bar
        order by c desc limit 1
    ) c(c)
;
 bar |      feh      
-----+---------------
 A   | {10,20,NULL}
 B   | {3,4,NULL}
 C   | {5,NULL,NULL}
 D   | {6,7,8}
Run Code Online (Sandbox Code Playgroud)