2

给定下表:

CREATE TABLE cnts(
  user_id INT,
  month_d DATE,
  cnt INT
)

我想查询每个 (user_id, month_d) 对的最后 6 个月的累积计数。我可以通过以下 JOIN 来做到这一点:

SELECT
  S1.month_d AS "month_d",
  S1.user_id AS "user_id",
  SUM(S2.cnt) AS "last_6_months_cnt"
FROM cnts S1
LEFT JOIN cnts S2 ON S1.user_id = S2.user_id
                 AND (S2.month_d BETWEEN (S1.month_d - INTERVAL '5 MONTH') AND S1.month_d)
GROUP BY 1, 2
ORDER BY 2, 1;

但我想知道这是否可以用窗口函数解决?

样本数据:

INSERT INTO cnts(user_id, month_d, cnt) VALUES 
(1, '2013-01-01', 2),
(1, '2013-04-01', 2),
(1, '2013-07-01', 2),
(1, '2013-10-01', 2),

(2, '2013-01-01', 2),
(2, '2013-04-01', 2),
(2, '2013-07-01', 2),
(2, '2013-10-01', 2)
;

预期结果(来自上面的连接):

  month_d   | user_id | last_6_months_cnt 
------------+---------+-------------------
 2013-01-01 |       1 |                 2
 2013-04-01 |       1 |                 4
 2013-07-01 |       1 |                 4
 2013-10-01 |       1 |                 4
 2013-01-01 |       2 |                 2
 2013-04-01 |       2 |                 4
 2013-07-01 |       2 |                 4
 2013-10-01 |       2 |                 4
4

1 回答 1

4

PostgreSQL 12 及更新版本

更新:PostgreSQL 12 和更新版本现在支持RANGEwindows。

正确的方法是使用一个窗口RANGE (INTERVAL '6' MONTH) PRECEDING

demo=> SELECT month_d, user_id, 
              SUM(cnt) OVER (PARTITION BY user_id ORDER BY month_d RANGE INTERVAL '6' MONTH PRECEDING)
       FROM cnts ORDER BY 2,1;

  month_d   | user_id | sum 
------------+---------+-----
 2013-01-01 |       1 |   2
 2013-04-01 |       1 |   4
 2013-07-01 |       1 |   6
 2013-10-01 |       1 |   6
 2013-01-01 |       2 |   2
 2013-04-01 |       2 |   4
 2013-07-01 |       2 |   6
 2013-10-01 |       2 |   6
(8 rows)

PostgreSQL 11 及更早版本

在 PostgreSQL 11 或更早版本RANGE上尚不支持 Windows,因此查询将失败:

regress=> SELECT month_d, user_id, 
          SUM(cnt) OVER (PARTITION BY user_id ORDER BY month_d RANGE INTERVAL '6' MONTH PRECEDING) 
          FROM cnts ORDER BY 2,1;
ERROR:  RANGE PRECEDING is only supported with UNBOUNDED
LINE 1: ...(cnt) OVER (PARTITION BY user_id ORDER BY month_d RANGE INTE...

没有它,您将返回 join over generate_series,并且在多个用户 ID 上执行此操作很麻烦。我怀疑您的自联接方法比尝试使用ROWS基于 window over来执行此操作要好得多sum。您必须generate_series将整个日期范围的 a 与所有不同 uid 的集合交叉连接,然后将其与cnts表左外连接,sum在窗口上处理它,然后过滤掉具有空计数的行。不用说,这是一种比简单的自加入更折磨人的做事方式。


对于您的示例数据,以下查询将产生与上面显示的相同的结果:

-- This query is totally wrong and only works because of overly simple sample data
SELECT 
  month_d, user_id, 
  SUM(cnt) OVER (PARTITION BY user_id ORDER BY month_d ROWS 1 PRECEDING)
FROM cnts
ORDER BY 2,1;

然而,这是完全错误的。我展示它主要是为了说明样本数据不足以进行可靠的测试,因为结果基本上是靠运气来匹配的。在六个月的范围内,您的所有样本都没有超过两个样本。示例数据很棒,但您需要考虑极端情况,就像编写单元测试时一样。您应该拥有不同的 uid,它们不会在相同的日期开始和停止,具有不同的计数等。

于 2013-03-26T08:28:50.670 回答