postgresql 使用CASE和GROUP BY动态替代透视

ne5o7dgx  于 2022-12-23  发布在  PostgreSQL
关注(0)|答案(6)|浏览(333)

我有一个类似这样的表:

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

这是一个非常make-shifty的方法,如果有很多新列要创建,就会变得很笨拙。我想知道CASE语句是否可以做得更好,使这个查询更动态?而且,我很想看到其他方法来做这件事。

2wnc66cl

2wnc66cl1#

如果您尚未安装附加模块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?

这里的特殊困难是,在基表中没有 * category *(cat)。对于基本的1-参数形式,我们可以只提供一个具有伪值的伪列作为category。无论如何,该值都被忽略。
这是一个罕见的情况,其中crosstab()函数的第二个参数不需要,因为根据这个问题的定义,所有NULL值只出现在右边的悬空列中,并且顺序可以由 * value * 确定。
如果我们有一个实际的 * category * 列,其名称决定了结果中值的顺序,我们需要crosstab()2参数形式,这里我借助窗口函数row_number()合成了一个category列,作为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?

剩下的都是些普通的问题。在这些紧密相关的答案中找到更多的解释和链接。

  • 基础知识:*
    如果您不熟悉crosstab()函数,请先阅读本文!
  • PostgreSQL交叉表查询
  • 高级:*
  • 使用Tablefunc透视多列
  • 在PostgreSQL中将表和更改日志合并到视图中

正确的测试设置

问题中缺少的测试设置:

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()调用提供了一个简单的基础结构,而无需提供列定义列表。

**crosstabN()**

crosstab1()-crosstab4()是预定义的。它们需要并返回所有的text,所以我们需要强制转换integer的值,但是这样可以简化调用:

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

结果:
| 行名称|类别_1|类别_2|类别_3|类别_4|
| - ------| - ------| - ------| - ------| - ------|
| A类|十个|二十个|||
| 乙|三个|四个|||
| C级|五个||||
| D级|六个|七|八个||

自定义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');

结果:
| 行名称|值1| val2值|val3| val4| val5|
| - ------| - ------| - ------| - ------| - ------| - ------|
| A类|十个|二十个||||
| 乙|三个|四个||||
| C级|五个|||||
| D级|六个|七|八个|||

* 一个 * 多态动态函数适用于所有

  • 这超出了tablefunc模块的范围。*

为了使返回类型动态化,我使用了多态类型,相关答案中详细介绍了这种技术:

  • 重构PL/pgSQL函数以返回各种SELECT查询的输出

1-参数形式:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement
  LANGUAGE plpgsql 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$;

2参数形式的此变量的过载:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement
  LANGUAGE plpgsql 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$;

pg_typeof(_rowtype)::text::regclass:每个用户定义的复合类型都定义了一个行类型,因此属性(列)列在系统目录pg_attribute中。将注册的类型(regtype)强制转换为text,并将此text强制转换为regclass

创建复合类型一次

对于要使用的每个返回类型,您都需要定义一次:

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

这个一个函数适用于所有返回类型,而tablefunc模块提供的crosstabN()框架需要为每种类型使用一个单独的函数。
如果你已经像上面演示的那样按顺序命名了你的类型,你只需要替换粗体数字。要找到基表中类别的最大数量:

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

如果你想要 * 单独的列 *,这就像这样的动态,像demonstrated by @Clocoaldo这样的数组,或者简单的文本表示,或者 Package 在jsonhstore这样的文档类型中的结果,都可以动态地适用于任何数量的类别。

    • 免责声明:**

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

致电原始问题:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);
x4shl7ld

x4shl7ld2#

虽然这是一个老问题,但我想添加另一个解决方案,它是通过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

转换为以下格式:

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

传统的解决方案是创建动态交叉表查询的一种聪明(而且知识渊博)的方法,ErwinBrandstetter的回答中对此进行了非常详细的解释。
但是,如果您的特定用例足够灵活,可以接受稍有不同的结果格式,那么另一种解决方案也是可行的,它可以完美地处理动态透视。

使用PostgreSQL的新jsonb_object_agg函数以JSON对象的形式动态构建透视数据。
我将用布兰德斯泰特先生的"更简单的测试案例"来说明:

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

使用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;

其输出:

bar |                  data                  
-----+----------------------------------------
 A   | { "val1" : 10, "val2" : 20 }
 B   | { "val1" : 3, "val2" : 4 }
 C   | { "val1" : 5 }
 D   | { "val3" : 8, "val1" : 6, "val2" : 7 }

正如您所看到的,这个函数的工作原理是从示例数据中的attribvalue列在JSON对象中创建键/值对,所有这些都按row_name分组。
虽然这个结果集看起来明显不同,但我相信它实际上可以满足许多(如果不是大多数)真实世界的用例,特别是那些数据需要动态生成透视表的用例,或者生成的数据由父应用程序使用的用例(例如,需要重新格式化以便在http响应中传输)。
这种方法的好处:

      • 更简洁的语法。**我想每个人都会同意,这种方法的语法比最基本的交叉表示例要简洁得多,也更容易理解。
      • 完全动态的。**不需要预先指定底层数据的信息。不需要预先知道列名或其数据类型。
      • 处理大量的列。**由于透视数据保存为单个jsonb列,所以不会遇到PostgreSQL的列限制(≤ 1,600列,我相信)。仍然有一个限制,但我相信它与文本字段相同:创建每个JSON对象需要1GB(如果我说错了,请纠正我)。这是很多键/值对!
      • 简化的数据处理。**我相信在DB中创建JSON数据将简化(并可能加速)父应用程序中的数据转换过程。(您会注意到,我们示例测试用例中的整数数据正确地存储在结果JSON对象中。PostgreSQL通过根据JSON规范自动将其固有数据类型转换为JSON来处理此问题。)这将有效地消除手动转换传递给父应用程序的数据的需要:都可以委托给应用程序的本地JSON解析器。

差异(和可能的缺点):

      • 它看起来不同。**不可否认,这种方法的结果看起来不同。JSON对象没有交叉表结果集漂亮;然而,这些差别纯粹是表面上的,产生了相同的信息--而且格式可能对父应用程序的使用更加友好。
      • 缺少键。**crosstab方法中缺少的值用空值填充,而JSON对象只是缺少适用的键。你必须自己决定这对于你的用例来说是否是一个可以接受的折衷。在我看来,在PostgreSQL中解决这个问题的任何尝试都将使过程变得非常复杂,并且可能涉及到一些额外查询形式的自省。
      • 键顺序不被保留。**我不知道这个问题是否可以在PostgreSQL中解决,但这个问题也大多是表面现象,因为任何父应用程序要么不太可能依赖键顺序,要么有能力通过其他方式确定正确的键顺序。最坏的情况可能只需要对数据库进行额外的查询。
    • 结论**

我很想听听别人的意见(特别是@ErwinBrandstetter's),尤其是在性能方面。当我在Andrew Bender的博客上发现这种方法时,就像被人打了一下脑袋。用一种新的方法解决PostrgeSQL中的难题是多么美妙的一种方式。它完美地解决了我的用例,我相信它同样也会为许多其他人服务。

slsn1g29

slsn1g293#

这是对@Damian good答案的补充。在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 }

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

jmo0nnb3

jmo0nnb34#

在您的情况下,我想数组是好的。SQL Fiddle

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}
ttvkxqim

ttvkxqim5#

很抱歉返回过去的数据,但是解决方案“动态交叉表”返回了错误的结果表。因此,valN值错误地“左对齐”,并且它们与列名不对应。当输入表中的值有“洞”时,例如“C”有val1和val3,但没有val2。这会产生错误:val3值将在最终表格的val2列(即下一个空闲列)中确定范围。

CREATE TEMP TABLE tbl (row_name text, attrib text, val int); 
INSERT INTO tbl (row_name, attrib, val) VALUES ('C', 'val1', 5) ('C', 'val3', 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
 C      |   5|  7 |

为了返回右列中有“洞”的正确单元格,交叉表查询需要在交叉表中执行第二次SELECT,类似于"crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2', 'select distinct row_name from tbl order by 1')"

gr8qqesn

gr8qqesn6#

从某种意义上说,这并不是真正的“动态”,因为您仍然需要枚举与预期值一样多的列,但通过这种方式很容易做到这一点。一个问题是,列需要一个序数键来匹配,没有中断。而且,如果有重复的键,它会使所有的键都不正常。因此也需要去重。这些集合都必须被预先分区以容纳N的适当集合。
对我来说,它看起来很笨拙,所以不确定它是否会带来很大的好处,但我把它添加到社区的狗堆中,希望它能为其他人提供一些刺激,让他们想出更好的方法。

/** build a dataset **/
DROP TABLE IF EXISTS tmpT ;
CREATE TEMP TABLE tmpT AS
SELECT
 NULL::INT AS key
 ,NULL::INT AS ints
 ,NULL::VARCHAR(1) AS chars
 ,NULL::VARCHAR(3) AS unnest
LIMIT 0 ;

insert into tmpT (key, ints, chars, unnest)
values   (1 , 1   , 'o',  CHR( 130 - 10 ) )       
        ,(2 , 2   , 'n',  CHR( 130 - 11 ) )       
        ,(3 , 3   , 'm',            NULL  )       
        --,(4 , 4   , 'l',  CHR( 130 - 13 ) ) -- missing set       
        ,(5 , 5   , null, CHR( 130 - 14 ) )        
        ,(6 , null, 'j',  CHR( 130 - 15 ) )        
        ,(7 , 7   , null, CHR( 130 - 16 ) )         
        ,(8 , null, 'h',  CHR( 130 - 17 ) )        
        ,(9 , 9   , null, CHR( 130 - 18 ) )         
        ,(10, null, 'f' ,           NULL  )        
        ,(11, null, 'a',  CHR( 130 - 20 ) )        
        ,(12, 12  , null, CHR( 130 - 21 ) )         
 ; /** end of build a dataset **/

/** set up full set of pivotal column positions, to backfill any missing  **/
DROP TABLE IF EXISTS tGenSer ; 
CREATE TEMP TABLE tGenSer AS SELECT generate_series( 1, 1000 )::INT AS key ;

/然后是枢轴/

/* Pivot 10 columns */
SELECT *
FROM     /* name the columns*/
(    SELECT a a ,a b ,a c ,a d ,a e ,a f ,a g ,a h ,a i ,a j /*,a k ,a l ,a m ,a n ,a o ,a p ,a q ,a r ,a s ,a t*/ /* ,a u ,a v ,a w ,a x ,a y ,a z*/ FROM ( SELECT NULL::VARCHAR(3) AS a /**seed the typed columns **/) a  
    
    UNION /** union is just a helper, to assign names to unnamed columns **/
    
    /** 20 columns **/
    SELECT * FROM
    (
        /* enumerate columns, no name */
        SELECT t1.x[1 ] ,t1.x[2 ] ,t1.x[3 ] ,t1.x[4 ] ,t1.x[5 ] ,t1.x[6 ] ,t1.x[7 ] ,t1.x[8 ] ,t1.x[9 ] ,t1.x[10] 
        FROM ( SELECT  ARRAY( SELECT  a.ints::TEXT  AS v   
                        FROM tGenSer tg /**backfill missing keys**/ 
                        LEFT JOIN tmpT a ON tg.key = a.key ORDER BY tg.key 
                        ) AS x 
             ) t1
        
        UNION ALL
        
        SELECT t1.x[1 ] ,t1.x[2 ] ,t1.x[3 ] ,t1.x[4 ] ,t1.x[5 ] ,t1.x[6 ] ,t1.x[7 ] ,t1.x[8 ] ,t1.x[9 ] ,t1.x[10] 
        FROM ( SELECT  ARRAY( SELECT  a.chars::TEXT AS v 
                        FROM tGenSer tg /**backfill missing keys**/ 
                        LEFT JOIN tmpT a ON tg.key = a.key ORDER BY tg.key 
                        ) AS x 
            ) t1
        
        UNION ALL
        
        SELECT t1.x[1 ] ,t1.x[2 ] ,t1.x[3 ] ,t1.x[4 ] ,t1.x[5 ] ,t1.x[6 ] ,t1.x[7 ] ,t1.x[8 ] ,t1.x[9 ] ,t1.x[10] 
        FROM ( SELECT  ARRAY( SELECT  a.unnest      AS v 
                        FROM tGenSer tg /**backfill missing keys**/ 
                        LEFT JOIN tmpT a ON tg.key = a.key 
                        ORDER BY tg.key 
                        ) AS x 
           ) t1
     ) a
)b
WHERE ( a,b,c,d,e,f,g,h,i,j) IS DISTINCT FROM ( NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL )        

;

结果:

+---+---+--+--+--+--+--+--+--+--+
| a | b |c |d |e |f |g |h |i |j |
+---+---+--+--+--+--+--+--+--+--+
| x | w |  |  |t |s |r |q |p |  |
| o | n |m |  |  |j |  |h |  |f |
| 1 | 2 |3 |  |5 |  |7 |  |9 |  |
+---+---+--+--+--+--+--+--+--+--+

相关问题