过去 X 个月内的 PostgreSQL 累积计数
Posted
技术标签:
【中文标题】过去 X 个月内的 PostgreSQL 累积计数【英文标题】:PostgreSQL cumulative count within last X months 【发布时间】:2013-03-26 07:30:07 【问题描述】:给定下表:
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
【问题讨论】:
我认为您遗漏了一些示例数据,因为出现的唯一 user_id 是 user_id 1。该示例数据不可能生成显示的结果表。 在感兴趣的时间范围内,每个(user_id, month_d)
对是否有总是一行?或者您是否假设丢失的行数为零? (如果总是有一行那么有更有效的方法来做到这一点)。
@CraigRinger 对不起!第二批插入应该有 user_id = 2。更新!
@CraigRinger 无法保证。但是当每对总是有行时,我很想知道解决方案。
不幸的是,最干净的基于窗口函数的解决方案需要在窗口框架定义中支持 RANGE
定义,不幸的是 PostgreSQL 在这一点上只支持 ROWS
。这仍然是可能的,但它可能需要加入generate_series
,如果它没有更好,我不会感到惊讶。如果时间允许,我还会有戏。
【参考方案1】:
PostgreSQL 12 及更新版本
更新:PostgreSQL 12 及更新版本现在支持RANGE
windows。
正确的方法是使用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
上尚不支持窗口,因此查询将失败:
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...
如果不这样做,您将通过generate_series
加入,并且在多个用户ID 上执行此操作很麻烦。我怀疑您的自联接方法比尝试使用基于ROWS
的窗口而不是sum
更可取。您必须将整个日期范围的 generate_series
与所有不同 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,它们不会在相同的日期开始和停止,具有不同的计数等。
【讨论】:
非常感谢克雷格的详细尝试。而且您在过于简单的示例上是对的。欣赏建议!我假设如果我真的有每个(user_id, month_d)
的数据,我可以使用ROWS 5 PRECEDING
来解决这个问题?
@huy 是的,但是缺少值会导致错误的结果。
@huy 顺便说一句,这个问题真的是根本问题“我如何更快地做到这一点”,尝试将其作为一个新问题发布,其中包含更好的样本数据集和一些 EXPLAIN ANALYZE
来自您的结果真实查询以及postgresql-performance 标签维基上提到的其他信息。
作为一个仅供参考,因为这是我搜索时最适合我的 Google 搜索,所以 PostgreSQL(我有 11 个)现在确实支持有界 RANGE
窗口。因此,上面发布的解决方案将起作用。
仅供参考,我相信这甚至可以在 PostgreSQL 11 上运行,您是否在 11 上尝试过但失败了?以上是关于过去 X 个月内的 PostgreSQL 累积计数的主要内容,如果未能解决你的问题,请参考以下文章
在 Bigquery 中计算过去 3 个月内活跃的供应商数量