概括 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 查询是不可能的。 如果你真的需要,你需要一个两步操作流程(到服务器的两次往返) :

  1. 构建查询。
  2. 执行它。

嗯,你 可以 准备 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;

dbfiddle

中的 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