【问题标题】:Generalizing a CASE statement (with dynamic names for result columns?)概括 CASE 语句(使用结果列的动态名称?)
【发布时间】:2021-09-24 17:09:22
【问题描述】:

我目前有一个这样的查询 -

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 语句或 @987654323 中的日期每月@子句。

我期待的输出

Jan  Feb  Mar  Apr  May  June
2    2    3    6    1    4

【问题讨论】:

  • 我认为使用 GETDATE () 函数然后你减去 1 个月这样每次它到达下个月你总是有你的 6 个月
  • 您可以为此声明一些变量并通过 SP 运行
  • 如果您还有日期列,为什么要将月份存储在单独的列中?
  • “一月”,“二月”,...但是“四月”?不是“四月”?
  • 它可以是AprApril,只要我可以每个月运行它而无需更改一堆东西。 @ErwinBrandstetter

标签: sql postgresql pivot aggregate dynamic-sql


【解决方案1】:

month 列似乎是多余的。把它从桌子上放下。 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小提琴here

每月返回一行,按时间顺序(也考虑到年份,尽管它不在您的输出中!),并且是真正动态的。

month ct
Jan 31
Feb 28
Mar 31
Apr 0
May 31
Jun 30

请注意我如何在第一个子查询mon 中使用generate_series() 首次构建过去六个月(不包括当前)的时间戳。见:

然后LEFT JOIN 从相关时间范围每月计数。这种方式总是返回最后 6 个月,即使根本没有找到任何行。 COALESCE 在这种情况下将计数设为 0 而不是 NULL。相关:

请特别注意,先聚合再加入会更快。见:

使用标准英文月份名称和 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($1)::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);

然后简单的函数调用就可以正常工作并返回您想要的结果:

SELECT * FROM f_tbl_counts_6months(NULL::m6_jul);
Jan Feb Mar Apr May Jun
31 28 31 0 31 30

为什么?如何?见:

您需要使用正确的类型来调用。我内置了一个故障保险装置以防止错误的结果。如果您使用错误的类型调用,例如 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

【讨论】:

  • 这与我期望的输出不一致。我已经用更新后的输出更新了问题。
  • 是的,我需要报表的透视表。
  • 我不能添加 12 个不同的 CASE 语句,因为这样报告将有 12 列。我只需要最近 6 个月的列。
  • @Aaron:核心问题是:你需要动态列名吗?这是棘手的部分。 (而且通常不是您想要的客户端代码!)其余的并不难。
  • 我确实需要动态列名。如果这不是一个选项,你会建议什么?
【解决方案2】:

从现在的日期返回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演示

【讨论】:

    【解决方案3】:

    试试这个:

    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
    

    【讨论】:

    • 我在 Postgres 上。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-02-27
    • 2012-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-22
    • 1970-01-01
    相关资源
    最近更新 更多