使用交叉表和计数来透视 table

Pivot table using crosstab and count

我必须像这样显示 table:

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.

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 值的完整动态解决方案:

此提议是 A and B 中提议的 crosstab 解决方案的替代解决方案。

这里提出的解决方案只需要一个可以动态创建的专用 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 中的完整演示。

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

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

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

  • PostgreSQL Crosstab Query

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;

如果 table invoices 只有 几个 个不同的标记值(这似乎很可能) 有更快的方法来获得不同的值。参见:

  • Optimize GROUP BY query to retrieve latest row per user

生成以下形式的查询:

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

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

  • Pivot on Multiple Columns using Tablefunc

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

  • How to get rows by max(date) group by Year-Month in Postgres?

另请注意美元引号的使用以避免引用地狱。参见:

  • Insert text with single quotes in PostgreSQL

没有条目的月份不会显示在结果中,并且现有月份的标记不会显示为 NULL。如果需要,您可以适应其中任何一个。参见:

  • Join a count query on generate_series() and retrieve Null values as '0'

然后执行生成的查询。

db<>fiddle here(重复使用 Edouard 的 fiddle,荣誉!)

参见:

在 psql 中

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

  • Simulate CREATE DATABASE IF NOT EXISTS for PostgreSQL?

在 Postgres 9.6 或更高版本中,您还可以使用元命令 \crosstabview 代替 of 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().

参见: