概括 CASE 语句(结果列的动态名称?)
Generalizing a CASE statement (with dynamic names for result columns?)
我目前有这样的查询 -
SELECT
SUM(CASE month WHEN 'January' THEN 1 ELSE 0 END) AS "Jan",
SUM(CASE month WHEN 'February' THEN 1 ELSE 0 END) AS "Feb",
SUM(CASE month WHEN 'March' THEN 1 ELSE 0 END) AS "Mar",
SUM(CASE month WHEN 'April' THEN 1 ELSE 0 END) AS "April",
SUM(CASE month WHEN 'May' THEN 1 ELSE 0 END) AS "May",
SUM(CASE month WHEN 'June' THEN 1 ELSE 0 END) AS "June"
FROM tbl
WHERE start >= '2021-01-01'
AND start <= '2021-06-30'
此查询每个月需要 运行 最近 6 个月。例如,由于当前月份尚未结束,此查询需要从 1 月 1 日到 6 月 30 日 运行。如何自动执行此查询,这样我就不必更改 CASE
语句或WHERE
子句中每个月的日期。
我期待的输出
Jan Feb Mar Apr May June
2 2 3 6 1 4
month
栏似乎是多余的。从 table 中删除它。 start
拥有您需要的所有信息。
(我宁愿不使用 start
作为列名,因为那是一个 keyword in standard SQL - 即使在 Postgres 中允许。)
SELECT date_trunc('month', start) AS mon, count(*) AS ct
FROM tbl
WHERE start >= '2021-01-01'
AND start < '2021-07-01'
GROUP BY 1
ORDER BY 1;
使用date_trunc()
保留时间顺序。如果您需要在结果中使用月份名称:
WITH cte(current_mon) AS (SELECT date_trunc('month', LOCALTIMESTAMP))
SELECT to_char(mon, 'Mon') AS month, COALESCE(data.ct, 0) AS ct
FROM cte c
CROSS JOIN generate_series(c.current_mon - interval '6 mon'
, c.current_mon - interval '1 mon'
, interval '1 mon') mon
LEFT JOIN (
SELECT date_trunc('month', start) AS mon, count(*)::int AS ct
FROM tbl, cte c
WHERE start >= c.current_mon - interval '6 mon'
AND start < c.current_mon
GROUP BY 1
) data USING (mon)
ORDER BY mon;
db<>fiddle here
Returns 每月一行,按时间顺序排列(也要考虑年份,尽管它不在您的输出中!),真正动态。
month
ct
Jan
31
Feb
28
Mar
31
Apr
0
May
31
Jun
30
请注意我是如何在第一个子查询 mon
中使用 generate_series()
首次构建过去六个月(不包括当前月份)的时间戳的。参见:
- Generating time series between two dates in PostgreSQL
然后 LEFT JOIN
从相关时间范围开始每月计数。这种方式总是 returns 最近 6 个月,即使根本没有找到任何行。 COALESCE
在这种情况下使计数为 0 而不是 NULL。相关:
- PostgreSQL: running count of rows for a query 'by minute'
请特别注意,先聚合后加入速度更快。参见:
- Query with LEFT JOIN not returning rows for count of 0
使用标准的英文月份名称和 3 个字母的缩写。
您的原始查询以旋转形式生成该信息:每列一个月。 但是动态列名对于静态 SQL 查询是不可能的。 如果你真的需要,你需要一个两步操作流程(到服务器的两次往返) :
- 构建查询。
- 执行它。
嗯,你 可以 准备 12 种不同的行类型(这是你的情况可能的结果类型的范围)并使用多态功能来实现它。但是你真的需要旋转形式吗?
好的,你自找的...
你想要这样一个简单的电话吗?
SELECT * FROM f_tbl_counts_6months(NULL::m6_jul);
有可能。这是一个概念证明。
但是,老实说,我宁愿避免复杂化,而只使用上面的简单查询。
创建多态函数:
CREATE OR REPLACE FUNCTION f_tbl_counts_6months(ANYELEMENT)
RETURNS SETOF ANYELEMENT
LANGUAGE plpgsql AS
$func$
DECLARE
_current_mon timestamp := date_trunc('month', LOCALTIMESTAMP);
BEGIN
-- to prevent incorrect column names, input row type must match current date:
IF right(pg_typeof()::text, 3) = to_char(_current_mon, 'mon') THEN
-- all good!
ELSE
RAISE EXCEPTION 'Current date is %. Function requires input >>%<<'
, CURRENT_DATE, 'NULL::m6_' || to_char(now(), 'mon');
END IF;
RETURN QUERY
SELECT a[2], a[2], a[3], a[4], a[5], a[6]
FROM (
SELECT ARRAY(
SELECT COALESCE(data.ct, 0)
FROM generate_series(_current_mon - interval '6 mon'
, _current_mon - interval '1 mon'
, interval '1 mon') mon
LEFT JOIN (
SELECT date_trunc('month', start) AS mon, count(*)::int AS ct
FROM tbl
GROUP BY 1
) data USING (mon)
ORDER BY mon
)
) sub(a);
END
$func$;
还有 12 种复合(行)类型,一种代表一年中的每个月:
CREATE TYPE m6_jan AS ("Jul" int, "Aug" int, "Sep" int, "Oct" int, "Nov" int, "Dec" int);
CREATE TYPE m6_feb AS ("Aug" int, "Sep" int, "Oct" int, "Nov" int, "Dec" int, "Jan" int);
CREATE TYPE m6_mar AS ("Sep" int, "Oct" int, "Nov" int, "Dec" int, "Jan" int, "Feb" int);
CREATE TYPE m6_apr AS ("Oct" int, "Nov" int, "Dec" int, "Jan" int, "Feb" int, "Mar" int);
CREATE TYPE m6_may AS ("Nov" int, "Dec" int, "Jan" int, "Feb" int, "Mar" int, "Apr" int);
CREATE TYPE m6_jun AS ("Dec" int, "Jan" int, "Feb" int, "Mar" int, "Apr" int, "May" int);
CREATE TYPE m6_jul AS ("Jan" int, "Feb" int, "Mar" int, "Apr" int, "May" int, "Jun" int);
CREATE TYPE m6_aug AS ("Feb" int, "Mar" int, "Apr" int, "May" int, "Jun" int, "Jul" int);
CREATE TYPE m6_sep AS ("Mar" int, "Apr" int, "May" int, "Jun" int, "Jul" int, "Aug" int);
CREATE TYPE m6_oct AS ("Apr" int, "May" int, "Jun" int, "Jul" int, "Aug" int, "Sep" int);
CREATE TYPE m6_nov AS ("May" int, "Jun" int, "Jul" int, "Aug" int, "Sep" int, "Oct" int);
CREATE TYPE m6_dec AS ("Jun" int, "Jul" int, "Aug" int, "Sep" int, "Oct" int, "Nov" int);
然后简单的函数调用就起作用了,returns 正是您想要的结果:
SELECT * FROM f_tbl_counts_6months(NULL::m6_jul);
Jan
Feb
Mar
Apr
May
Jun
31
28
31
0
31
30
为什么?如何?参见:
- Refactor a PL/pgSQL function to return the output of various SELECT queries
您需要使用正确的类型来调用。我内置了一个故障安全装置来防止错误的结果。如果您调用错误类型,例如 7 月(当前)的以下调用:
SELECT * FROM f_tbl_counts_6months(NULL::m6_nov);
...函数抛出异常,说明:
ERROR: Current date is 2021-07-15. Function requires input >>NULL::m6_jul<<
CONTEXT: PL/pgSQL function f_tbl_counts_6months(anyelement) line 9 at RAISE
从现在的日期返回 1
个月:结果是结束日期:DATEADD(MONTH, -1, GETDATE())
7
个月后开始日期:DATEADD(MONTH, -7, GETDATE())
要计算该月的第一天,请从其自身中减去今天:(DAY(CURRENT_TIMESTAMP) - 1)
获取结束日期
sql => select DATEADD(MONTH, -1, GETDATE()) - (DAY(CURRENT_TIMESTAMP) - 1)
postgresql => SELECT now() - INTERVAL '1 month' - (extract(day from now()) - 1 || ' day')::INTERVAL;
获取开始日期
sql => select DATEADD(MONTH, -7, GETDATE()) - (DAY(CURRENT_TIMESTAMP) - 1)
postgresql => select now() - INTERVAL '7 month' - (extract(day from now()) - 1 || ' day')::INTERVAL;
查询
SELECT
SUM(CASE month WHEN 'January' THEN 1 ELSE 0 END) AS "Jan",
SUM(CASE month WHEN 'February' THEN 1 ELSE 0 END) AS "Feb",
SUM(CASE month WHEN 'March' THEN 1 ELSE 0 END) AS "Mar",
SUM(CASE month WHEN 'April' THEN 1 ELSE 0 END) AS "April",
SUM(CASE month WHEN 'May' THEN 1 ELSE 0 END) AS "May",
SUM(CASE month WHEN 'June' THEN 1 ELSE 0 END) AS "June"
FROM dateTable
WHERE start >= now() - INTERVAL '7 month' - (extract(day from now()) - 1 || ' day')::INTERVAL
AND start <= now() - INTERVAL '1 month' - (extract(day from now()) - 1 || ' day')::INTERVAL;
中的 postgresql 演示
试试这个:
DECLARE @STARTDATE AS DATE = DATEADD(d, -31, DATEADD(m, DATEDIFF(m, -1, GETDATE()) - 6, 0))
DECLARE @ENDDATE AS DATE = DATEADD(d, -1, DATEADD(m, DATEDIFF(m, -1, GETDATE()) - 1, 0))
SELECT
SUM(CASE month WHEN 'January' THEN 1 ELSE 0 END) AS "Jan",
SUM(CASE month WHEN 'February' THEN 1 ELSE 0 END) AS "Feb",
SUM(CASE month WHEN 'March' THEN 1 ELSE 0 END) AS "Mar",
SUM(CASE month WHEN 'April' THEN 1 ELSE 0 END) AS "April",
SUM(CASE month WHEN 'May' THEN 1 ELSE 0 END) AS "May",
SUM(CASE month WHEN 'June' THEN 1 ELSE 0 END) AS "June"
WHERE start >= @STARTDATE
AND start <= @ENDDATE
我目前有这样的查询 -
SELECT
SUM(CASE month WHEN 'January' THEN 1 ELSE 0 END) AS "Jan",
SUM(CASE month WHEN 'February' THEN 1 ELSE 0 END) AS "Feb",
SUM(CASE month WHEN 'March' THEN 1 ELSE 0 END) AS "Mar",
SUM(CASE month WHEN 'April' THEN 1 ELSE 0 END) AS "April",
SUM(CASE month WHEN 'May' THEN 1 ELSE 0 END) AS "May",
SUM(CASE month WHEN 'June' THEN 1 ELSE 0 END) AS "June"
FROM tbl
WHERE start >= '2021-01-01'
AND start <= '2021-06-30'
此查询每个月需要 运行 最近 6 个月。例如,由于当前月份尚未结束,此查询需要从 1 月 1 日到 6 月 30 日 运行。如何自动执行此查询,这样我就不必更改 CASE
语句或WHERE
子句中每个月的日期。
我期待的输出
Jan Feb Mar Apr May June
2 2 3 6 1 4
month
栏似乎是多余的。从 table 中删除它。 start
拥有您需要的所有信息。
(我宁愿不使用 start
作为列名,因为那是一个 keyword in standard SQL - 即使在 Postgres 中允许。)
SELECT date_trunc('month', start) AS mon, count(*) AS ct
FROM tbl
WHERE start >= '2021-01-01'
AND start < '2021-07-01'
GROUP BY 1
ORDER BY 1;
使用date_trunc()
保留时间顺序。如果您需要在结果中使用月份名称:
WITH cte(current_mon) AS (SELECT date_trunc('month', LOCALTIMESTAMP))
SELECT to_char(mon, 'Mon') AS month, COALESCE(data.ct, 0) AS ct
FROM cte c
CROSS JOIN generate_series(c.current_mon - interval '6 mon'
, c.current_mon - interval '1 mon'
, interval '1 mon') mon
LEFT JOIN (
SELECT date_trunc('month', start) AS mon, count(*)::int AS ct
FROM tbl, cte c
WHERE start >= c.current_mon - interval '6 mon'
AND start < c.current_mon
GROUP BY 1
) data USING (mon)
ORDER BY mon;
db<>fiddle here
Returns 每月一行,按时间顺序排列(也要考虑年份,尽管它不在您的输出中!),真正动态。
month | ct |
---|---|
Jan | 31 |
Feb | 28 |
Mar | 31 |
Apr | 0 |
May | 31 |
Jun | 30 |
请注意我是如何在第一个子查询 mon
中使用 generate_series()
首次构建过去六个月(不包括当前月份)的时间戳的。参见:
- Generating time series between two dates in PostgreSQL
然后 LEFT JOIN
从相关时间范围开始每月计数。这种方式总是 returns 最近 6 个月,即使根本没有找到任何行。 COALESCE
在这种情况下使计数为 0 而不是 NULL。相关:
- PostgreSQL: running count of rows for a query 'by minute'
请特别注意,先聚合后加入速度更快。参见:
- Query with LEFT JOIN not returning rows for count of 0
使用标准的英文月份名称和 3 个字母的缩写。
您的原始查询以旋转形式生成该信息:每列一个月。 但是动态列名对于静态 SQL 查询是不可能的。 如果你真的需要,你需要一个两步操作流程(到服务器的两次往返) :
- 构建查询。
- 执行它。
嗯,你 可以 准备 12 种不同的行类型(这是你的情况可能的结果类型的范围)并使用多态功能来实现它。但是你真的需要旋转形式吗?
好的,你自找的...
你想要这样一个简单的电话吗?
SELECT * FROM f_tbl_counts_6months(NULL::m6_jul);
有可能。这是一个概念证明。
但是,老实说,我宁愿避免复杂化,而只使用上面的简单查询。
创建多态函数:
CREATE OR REPLACE FUNCTION f_tbl_counts_6months(ANYELEMENT)
RETURNS SETOF ANYELEMENT
LANGUAGE plpgsql AS
$func$
DECLARE
_current_mon timestamp := date_trunc('month', LOCALTIMESTAMP);
BEGIN
-- to prevent incorrect column names, input row type must match current date:
IF right(pg_typeof()::text, 3) = to_char(_current_mon, 'mon') THEN
-- all good!
ELSE
RAISE EXCEPTION 'Current date is %. Function requires input >>%<<'
, CURRENT_DATE, 'NULL::m6_' || to_char(now(), 'mon');
END IF;
RETURN QUERY
SELECT a[2], a[2], a[3], a[4], a[5], a[6]
FROM (
SELECT ARRAY(
SELECT COALESCE(data.ct, 0)
FROM generate_series(_current_mon - interval '6 mon'
, _current_mon - interval '1 mon'
, interval '1 mon') mon
LEFT JOIN (
SELECT date_trunc('month', start) AS mon, count(*)::int AS ct
FROM tbl
GROUP BY 1
) data USING (mon)
ORDER BY mon
)
) sub(a);
END
$func$;
还有 12 种复合(行)类型,一种代表一年中的每个月:
CREATE TYPE m6_jan AS ("Jul" int, "Aug" int, "Sep" int, "Oct" int, "Nov" int, "Dec" int);
CREATE TYPE m6_feb AS ("Aug" int, "Sep" int, "Oct" int, "Nov" int, "Dec" int, "Jan" int);
CREATE TYPE m6_mar AS ("Sep" int, "Oct" int, "Nov" int, "Dec" int, "Jan" int, "Feb" int);
CREATE TYPE m6_apr AS ("Oct" int, "Nov" int, "Dec" int, "Jan" int, "Feb" int, "Mar" int);
CREATE TYPE m6_may AS ("Nov" int, "Dec" int, "Jan" int, "Feb" int, "Mar" int, "Apr" int);
CREATE TYPE m6_jun AS ("Dec" int, "Jan" int, "Feb" int, "Mar" int, "Apr" int, "May" int);
CREATE TYPE m6_jul AS ("Jan" int, "Feb" int, "Mar" int, "Apr" int, "May" int, "Jun" int);
CREATE TYPE m6_aug AS ("Feb" int, "Mar" int, "Apr" int, "May" int, "Jun" int, "Jul" int);
CREATE TYPE m6_sep AS ("Mar" int, "Apr" int, "May" int, "Jun" int, "Jul" int, "Aug" int);
CREATE TYPE m6_oct AS ("Apr" int, "May" int, "Jun" int, "Jul" int, "Aug" int, "Sep" int);
CREATE TYPE m6_nov AS ("May" int, "Jun" int, "Jul" int, "Aug" int, "Sep" int, "Oct" int);
CREATE TYPE m6_dec AS ("Jun" int, "Jul" int, "Aug" int, "Sep" int, "Oct" int, "Nov" int);
然后简单的函数调用就起作用了,returns 正是您想要的结果:
SELECT * FROM f_tbl_counts_6months(NULL::m6_jul);
Jan | Feb | Mar | Apr | May | Jun |
---|---|---|---|---|---|
31 | 28 | 31 | 0 | 31 | 30 |
为什么?如何?参见:
- Refactor a PL/pgSQL function to return the output of various SELECT queries
您需要使用正确的类型来调用。我内置了一个故障安全装置来防止错误的结果。如果您调用错误类型,例如 7 月(当前)的以下调用:
SELECT * FROM f_tbl_counts_6months(NULL::m6_nov);
...函数抛出异常,说明:
ERROR: Current date is 2021-07-15. Function requires input >>NULL::m6_jul<<
CONTEXT: PL/pgSQL function f_tbl_counts_6months(anyelement) line 9 at RAISE
从现在的日期返回 1
个月:结果是结束日期:DATEADD(MONTH, -1, GETDATE())
7
个月后开始日期:DATEADD(MONTH, -7, GETDATE())
要计算该月的第一天,请从其自身中减去今天:(DAY(CURRENT_TIMESTAMP) - 1)
获取结束日期
sql => select DATEADD(MONTH, -1, GETDATE()) - (DAY(CURRENT_TIMESTAMP) - 1)
postgresql => SELECT now() - INTERVAL '1 month' - (extract(day from now()) - 1 || ' day')::INTERVAL;
获取开始日期
sql => select DATEADD(MONTH, -7, GETDATE()) - (DAY(CURRENT_TIMESTAMP) - 1)
postgresql => select now() - INTERVAL '7 month' - (extract(day from now()) - 1 || ' day')::INTERVAL;
查询
SELECT
SUM(CASE month WHEN 'January' THEN 1 ELSE 0 END) AS "Jan",
SUM(CASE month WHEN 'February' THEN 1 ELSE 0 END) AS "Feb",
SUM(CASE month WHEN 'March' THEN 1 ELSE 0 END) AS "Mar",
SUM(CASE month WHEN 'April' THEN 1 ELSE 0 END) AS "April",
SUM(CASE month WHEN 'May' THEN 1 ELSE 0 END) AS "May",
SUM(CASE month WHEN 'June' THEN 1 ELSE 0 END) AS "June"
FROM dateTable
WHERE start >= now() - INTERVAL '7 month' - (extract(day from now()) - 1 || ' day')::INTERVAL
AND start <= now() - INTERVAL '1 month' - (extract(day from now()) - 1 || ' day')::INTERVAL;
中的 postgresql 演示
试试这个:
DECLARE @STARTDATE AS DATE = DATEADD(d, -31, DATEADD(m, DATEDIFF(m, -1, GETDATE()) - 6, 0))
DECLARE @ENDDATE AS DATE = DATEADD(d, -1, DATEADD(m, DATEDIFF(m, -1, GETDATE()) - 1, 0))
SELECT
SUM(CASE month WHEN 'January' THEN 1 ELSE 0 END) AS "Jan",
SUM(CASE month WHEN 'February' THEN 1 ELSE 0 END) AS "Feb",
SUM(CASE month WHEN 'March' THEN 1 ELSE 0 END) AS "Mar",
SUM(CASE month WHEN 'April' THEN 1 ELSE 0 END) AS "April",
SUM(CASE month WHEN 'May' THEN 1 ELSE 0 END) AS "May",
SUM(CASE month WHEN 'June' THEN 1 ELSE 0 END) AS "June"
WHERE start >= @STARTDATE
AND start <= @ENDDATE