admin

使用 CASE 和 GROUP BY 旋转的动态替代方案

sql

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

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

我希望它看起来像这样:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

我有这样的查询:

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

这是一种非常灵活的方法,如果要创建大量新列,则它会变得笨拙。我想知道是否CASE可以更好地使此查询更具动态性?另外,我很想看到其他方法来做到这一点。


阅读 243

收藏
2021-07-01

共1个答案

admin

如果您尚未安装附加模块tablefunc,请为每个数据库运行一次此命令:

CREATE EXTENSION tablefunc;

回答问题

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

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?

这里的特殊困难在于,基表中没有类别( 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?

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

正确的测试设置

这就是您应该提供一个测试用例的方式:

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');

动态交叉表?

正如@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);

称呼:

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);

返回:

 row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8

tablefunc模块内置功能

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

crosstabN()

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

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

结果:

 row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |

自定义crosstab()功能

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

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

功能:

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

称呼:

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

结果:

 row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |

一种适用于所有人的多态动态函数

这超出了tablefunc模块所涵盖的范围。
为了使返回类型动态化,我使用多态类型和此相关答案中详述的技术:

1-参数形式:

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;

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

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;

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

一次创建复合类型:

您需要定义要使用的每个返回类型:

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);

...

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

CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);

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

称呼

使用上述行类型:

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

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

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

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

一个函数适用于所有返回类型,而模块提供的框架需要为每个返回一个单独的函数。 如果您已按照上面演示的顺序命名您的类型,则只需替换粗体数字。要在基表中查找最大类别数:crosstab*N*()``tablefunc

SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;

如果您想要单独的列,这将是动态的。像@Clocoaldo 演示的数组或简单的文本表示或包装在文档类型中的结果,例如jsonhstore可以动态地用于任意数量的类别。

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

原始问题:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);
2021-07-01