使用CASE和GROUP BY进行动态替代

我有一个如下所示的表格:

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语句可以更好地使这个查询更动态? 此外,我很乐意看到其他方法来做到这一点。

如果您还没有安装额外的模块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值仅显示在悬空列中。 订单可以由价值决定。

如果我们有一个实际的类别列,名称决定了结果中的值的顺序,我们需要使用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? 

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

基本:
如果您不熟悉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()调用提供了一个简单的基础结构,而不需要提供列定义列表。 用C编写的一些函数(通常非常快):

 crosstab N () 

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模块所涵盖的tablefunc
为了使返回类型动态我使用一个多态的类型与在这个相关的答案详细的技术:

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

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

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

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

虽然这是一个老问题,但是我想通过最近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 

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

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

  • 使用JSON和PostgreSQL的动态数据透视表

使用PostgreSQL的新jsonb_object_agg函数以JSON对象的形式即时构造jsonb_object_agg数据。

我会用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); 

使用jsonb_object_agg函数,我们可以用这个jsonb_object_agg来创建所需的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响应中传输)。

这种方法的好处:

  • 更清晰的语法。 我认为每个人都会同意,这种方法的语法比甚至是最基本的交叉表示例都要简单得多。

  • 完全动态。 事先不需要指定有关底层数据的信息。 不需要提前知道列名和数据类型。

  • 处理大量的列。 由于pivoted数据保存为一个单一的jsonb列,所以你不会碰到PostgreSQL的列限制(我认为≤1,600列)。 仍然有一个限制,但是我相信它和文本字段是一样的:创建每个JSON对象1 GB(如果我错了,请纠正我)。 这是很多关键/值对!

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

差异(和可能的缺点):

  • 看起来不一样 不可否认,这种方法的结果看起来不一样。 JSON对象不像交叉表结果集那么漂亮; 然而,差异纯粹是美容。 生成相同的信息 – 并且可能适合父应用程序使用的格式。

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

  • 按键顺序不保留。 我不知道这个问题是否可以在PostgreSQL中解决,但这个问题大部分都是表面化的,因为任何父应用程序都不太可能依赖于键顺序,或者有能力通过其他方式确定正确的键顺序。 最糟糕的情况可能只需要添加一个数据库查询。

结论

我很好奇听到别人的意见(特别是@ ErwinBrandstetter的)对这种方法的意见,尤其是因为它涉及到性能。 当我在安德鲁·本德的博客上发现这种方法时,就好像被打在了头上。 对于PostrgeSQL中的一个难题,采取一种全新的方法是一个很好的方法。 它完美地解决了我的用例,我相信它同样也会服务于其他许多用例。

在你的情况下,我猜数组是好的。 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} 

我很抱歉过去回来,但解决方案“动态交叉表”返回错误的结果表。 因此,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不同的行名从tbl顺序1')“

这是完成@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 } 

对于理解JSON的最终查询消费者,没有缺点。 唯一的一个是它不能作为表格来源消耗。