【问题标题】:Pivot table using crosstab and count使用交叉表和计数的数据透视表
【发布时间】:2022-01-24 09:04:04
【问题描述】:

我必须显示这样的表格:

Year Month Delivered Not delivered Not Received
2021 Jan 10 86 75
2021 Feb 13 36 96
2021 March 49 7 61
2021 Apr 3 21 72

使用此查询生成的原始数据:

SELECT 
    year,
    TO_CHAR( creation_date, 'Month') AS month,
    marking,
    COUNT(*) AS count 
FROM invoices
GROUP BY 1,2,3

我曾尝试使用crosstab(),但出现错误:

SELECT * FROM crosstab('
    SELECT 
        year,
        TO_CHAR( creation_date, ''Month'') AS month,
        marking,
        COUNT(*) AS count 
    FROM invoices
    GROUP BY 1,2,3
') AS ct(year text, month text, marking text)

我不想手动输入所有标记值,因为它们很多。

ERROR:  invalid source data SQL statement
DETAIL:  The provided SQL must return 3 columns: rowid, category, and values.

【问题讨论】:

  • 这两个答案表明,做 N 个动态列变得非常困难,特别是如果你想要 N 个 named 动态列... A1 A2 祝你好运!对于我自己的情况,我只是通过使用启用 jinja 模板的 dbt 学会了如何做到这一点 - 我的切线相关 answer 但语法略有不同(雪花)。
  • @sgoley。我同意你的观点,使用 pg 获得 N 个命名的动态列是一个有点困难的问题。我尝试在我的回答中提出一个完整的动态解决方案,它可以替代您在评论中提到的 crosstab 解决方案。

标签: postgresql pivot-table dynamic-sql crosstab postgres-crosstab


【解决方案1】:

1.具有有限marking 值列表的静态解决方案:

SELECT year
     , TO_CHAR( creation_date, 'Month') AS month
     , COUNT(*) FILTER (WHERE marking = 'Delivered') AS Delivered
     , COUNT(*) FILTER (WHERE marking = 'Not delivered') AS "Not delivered"
     , COUNT(*) FILTER (WHERE marking = 'Not Received') AS "Not Received"
FROM invoices
GROUP BY 1,2

2。具有大量 marking 值的完整动态解决方案:

此提案是crosstab 解决方案的替代解决方案,如AB 中提出的。

这里提出的解决方案只需要一个可以动态创建的专用composite type,然后它依赖于jsonb类型和标准函数:

从计算每年、每月和marking 值的行数的查询开始:

  • 使用jsonb_object_agg 函数,结果行是第一个 按年和月聚合成jsonb 对象,其jsonb keys 对应于marking 值和其jsonb values 对应计数。
  • 然后使用jsonb_populate_record 函数和专用复合类型将生成的jsonb 对象转换为记录。

首先我们动态创建一个composite type,它对应于marking值的有序列表:

CREATE OR REPLACE PROCEDURE create_composite_type() LANGUAGE plpgsql AS $$
DECLARE
  column_list text ;
BEGIN
  SELECT string_agg(DISTINCT quote_ident(marking) || ' bigint', ',' ORDER BY quote_ident(marking) || ' bigint' ASC)
    INTO column_list
    FROM invoices ;
  
  EXECUTE 'DROP TYPE IF EXISTS composite_type' ;
  EXECUTE 'CREATE TYPE composite_type AS (' || column_list || ')' ;
END ;
$$ ;

CALL create_composite_type() ;

那么预期的结果由以下查询提供:

SELECT a.year
     , TO_CHAR(a.year_month, 'Month') AS month
     , (jsonb_populate_record( null :: composite_type
                             , jsonb_object_agg(a.marking, a.count)
                             )
       ).*
 FROM
    ( SELECT year
           , date_trunc('month', creation_date) AS year_month
           , marking
           , count(*) AS count
        FROM invoices AS v
       GROUP BY 1,2,3
    ) AS a
GROUP BY 1,2
ORDER BY month

显然,如果marking 值列表可能随时间变化,那么您必须在执行查询之前调用create_composite_type() 过程。如果您不更新composite_type,查询仍然有效(没有错误!)但一些旧标记值可能已过时(不再使用),查询结果中可能缺少一些新标记值(未显示作为列)。

dbfiddle 中查看完整演示。

【讨论】:

  • 我必须使用交叉表,因为标记值很多,我更喜欢动态的东西作为数据透视表。
  • 这个提议的解决方案是完全动态的,它可以管理大量随时间变化的标记值,并且它避免使用数据透视表,所以我真的不明白它有什么问题.
【解决方案2】:

您需要动态生成crosstab() 调用。 但由于 SQL 不允许动态返回类型,您需要一个两步工作流程:

  1. 生成查询
  2. 执行查询

如果您不熟悉crosstab(),请先阅读以下内容:

creation_date 生成月份很奇怪,而不是年份。为简化起见,我改用组合列 year_month

查询以生成crosstab() 查询:

SELECT format(
$f$SELECT * FROM crosstab(
   $q$
   SELECT to_char(date_trunc('month', creation_date), 'YYYY_Month') AS year_month
        , marking
        , COUNT(*) AS ct
   FROM   invoices
   GROUP  BY date_trunc('month', creation_date), marking
   ORDER  BY date_trunc('month', creation_date)  -- optional
   $q$
 , $c$VALUES (%s)$c$
   ) AS ct(year_month text, %s);
$f$, string_agg(quote_literal(sub.marking), '), (')
   , string_agg(quote_ident  (sub.marking), ' int, ') || ' int'
)
FROM  (SELECT DISTINCT marking FROM invoices ORDER BY 1) sub;

如果表invoices,只有很少 个不同的值用于标记(这似乎很可能),那么有更快的方法来获得不同的值。见:

生成表单的查询:

SELECT * FROM crosstab(
   $q$
   SELECT to_char(date_trunc('month', creation_date), 'YYYY_Month') AS year_month
        , marking
        , COUNT(*) AS ct
   FROM   invoices
   GROUP  BY date_trunc('month', creation_date), marking
   ORDER  BY date_trunc('month', creation_date)  -- optional
   $q$
 , $c$VALUES ('Delivered'), ('Not Delivered'), ('Not Received')$c$
   ) AS ct(year_month text, "Delivered" int, "Not Delivered" int, "Not Received" int);

简化查询不需要“额外的列。见:

注意date_trunc('month', creation_date)GROUP BYORDER BY 中的使用。这会产生一个有效的排序顺序,而且速度也更快。见:

还要注意使用美元引号以避免引用地狱。见:

没有条目的月份不会显示在结果中,并且现有月份的任何标记都不会显示为NULL。如果需要,您可以进行调整。见:

然后执行生成的查询。

dbfiddle here(重用 爱德华的小提琴,赞!)

见:

在 psql 中

psql 中,您可以使用\qexec 立即执行生成的查询。见:

在 Postgres 9.6 或更高版本中,您还可以使用\crosstabview 代替 crosstab()

test=> SELECT to_char(date_trunc('month', creation_date), 'YYYY_Month') AS year_month
test->      , marking
test->      , COUNT(*) AS count
test-> FROM   invoices
test-> GROUP  BY date_trunc('month', creation_date), 2
test-> ORDER  BY date_trunc('month', creation_date)\crosstabview

   year_month   | Not Received | Delivered | Not Delivered 
----------------+--------------+-----------+---------------
 2020_January   |            1 |         1 |             1
 2020_March     |              |         2 |             2
 2021_January   |            1 |         1 |             2
 2021_February  |            1 |           |              
 2021_March     |              |         1 |              
 2021_August    |            2 |         1 |             1
 2022_August    |              |         2 |              
 2022_November  |            1 |         2 |             3
 2022_December  |            2 |           |              
(9 rows)

请注意,\crosstabview - 与 crosstab() 不同 - 不支持“额外”列。如果你坚持年份和月份分开,你需要crosstab()

见:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-24
    • 1970-01-01
    相关资源
    最近更新 更多