使用交叉表和计数来透视 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 类型,您需要一个两步工作流程:
- 生成查询
- 执行查询
如果您不熟悉 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 BY
和 ORDER 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()
.
参见:
我必须像这样显示 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 类型,您需要一个两步工作流程:
- 生成查询
- 执行查询
如果您不熟悉 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 BY
和 ORDER 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()
.
参见: