连接表上的窗口函数
Posted
技术标签:
【中文标题】连接表上的窗口函数【英文标题】:Window function on joined tables 【发布时间】:2018-01-08 16:37:25 【问题描述】:我想使用类似于here 的窗口函数的效率。在链接的示例中,我能够使用窗口函数,这样我就不必将表连接到自身上。加速是戏剧性的——大约从 O(n^2) 到 O(n)。在这个问题中,没有办法绕过连接,但我的两个表都非常大(数百万行),我想再次避免 O(n^2) 破坏数据。在这种情况下,窗口功能或类似功能是否仍然有效?
我有两张这样的表:
CREATE TABLE reports (
report_date DATE,
PRIMARY KEY (report_date));
CREATE TABLE time_series (
snapshot_date DATE,
sales INTEGER,
PRIMARY KEY (snapshot_date));
使用这样的值:
INSERT INTO time_series SELECT '2017-01-01'::DATE AS snapshot_date,10 AS sales;
INSERT INTO time_series SELECT '2017-01-02'::DATE AS snapshot_date,4 AS sales;
INSERT INTO time_series SELECT '2017-01-03'::DATE AS snapshot_date,13 AS sales;
INSERT INTO time_series SELECT '2017-01-04'::DATE AS snapshot_date,7 AS sales;
INSERT INTO time_series SELECT '2017-01-05'::DATE AS snapshot_date,15 AS sales;
INSERT INTO time_series SELECT '2017-01-06'::DATE AS snapshot_date,8 AS sales;
INSERT INTO reports SELECT '2017-01-03'::DATE AS report_date;
INSERT INTO reports SELECT '2017-01-06'::DATE AS report_date;
我想执行这样的连接(但更有效):
SELECT r.report_date,
SUM(sales) AS total_sales
FROM reports AS r
JOIN time_series AS ts
ON r.report_date > ts.snapshot_date
GROUP BY r.report_date
ORDER BY r.report_date
得到这样的结果:
*---------------*-------------*
| report_date | total_sales |
*---------------*-------------*
| 2017-01-03 | 14 |
| 2017-01-06 | 49 |
------------------------------*
【问题讨论】:
你已经拥有的东西效率低下是什么?鉴于数据的当前外观,一种替代方法可能是首先在time_series
上生成sales
的运行余额,然后对report_date
进行等值连接。
@Steve 什么是等值连接?虽然我可以在time_series
上进行sales
的运行平衡,但我想避免将sales
与reports
中的行数成比例地相乘。如果我选择sales
的排名最高/最近的运行余额,那是否仍然需要对reports
中每条记录的所有运行余额进行排序?窗口函数的吸引力在于它似乎只通过运行余额一次,而不是重复reports
中的每一行@
【参考方案1】:
@user554481,来自 cmets。
正如你所说,窗口函数可能在算法上更有效。
等值连接是与=
的连接,可找到直接匹配项(即最常见的连接类型,而不是与>
的非等连接)。
如果您对销售列进行求和,很明显我们现在只需要直接匹配。因此,加入report_date = snapshot_date
将为我们提供27
的运行总和2017-01-03
。
如果您只想要所有 之前 行的运行总和,那么您只需减去匹配日期的 sales
数字 - 在本例中为 13
,给我们您的结果想要27 - 13 = 14
。同样的逻辑也适用于2017-01-06
。
这当然取决于对于每个可能的report_date
有一个snapshot_date
,否则连接将失败。
我没有测试过这段代码(而且我对 Postgres 也不熟悉),但你明白了要点:
SELECT
r.report_date
,(ts.sales_run_sum - ts.sales) AS sales_prev_run_sum
FROM
reports AS r
LEFT JOIN
(
SELECT
snapshot_date
,sales
,SUM(sales) OVER (ORDER BY snapshot_date ASC) AS sales_run_sum
FROM
time_series
) AS ts
ON r.report_date = ts.snapshot_date
ORDER BY
r.report_date
编辑:顺便说一句,如果此报告定期运行但仅针对新报告日期,并且您说您有数百万行,那么您'最好的做法是在每次运行报表时缓存销售额的总和,然后在下次运行时仅选择 time_series
中比上一个缓存值更新的行,然后将缓存值作为偏移量添加到您对新的 time_series
值进行的运行总和。这是处理大容量数据时的基本方法,您需要一个运行平衡,并且在日期上有适当的索引。
编辑 2: 基于您的以下 cmets。那为什么这两个表中有“数百万行”呢?这种性质的数据似乎有点极端。
无论哪种方式,如果您不能保证快照表中每天至少有一行,那么可以考虑从日期表左连接,以确保每天至少有一行,甚至在time_series
中物理插入虚拟行(sales
图为零)以填补空白。
如果这两种方法都不可接受,那么您将不可避免地不得不以最初的方式实施不等式连接。
但仍然考虑我的解决方案的另一个方面,即缓存以前总和的结果。这允许您在加入 time_series
之前在其上引入 where 子句(基于仅选择 time_series
中的行,因为自最后一个缓存值创建以来),这将大大减少需要的行数每次运行查询时加入并求和。一旦您进入数百万行必须连接到数百万行的领域,这可能是唯一的高性能解决方案。
【讨论】:
很遗憾,我没有每个报告日期的快照日期。我只有状态变化时的快照日期 - 我在示例中提供的有限数据已被简化。 有几百万行,因为报表是按客户细分的,我们有几千个客户,实际上不是销售数据而是交易数据,所以在一段时间内有很多很多行20年。我引用了销售数据,因为它似乎是 RDBMS 类型问题的“hello world”等价物。无论如何,我担心左加入日期表可能会显着增加行数(因为快照日期实际上有些稀疏) 我希望有某种内置的 Postgres 窗口函数来有效地处理这些类型的不等式连接,但我赞成你的答案,因为它似乎是迄今为止最好的(唯一的)选项 @user554481,别忘了标记为已回答!如果您正在处理财务数据,请遵循我所描述的标准会计模式 - 生成定期汇总余额,然后将这些余额结转到正在考虑的下一个期间。此外,客户编号上的集群索引帐户数据然后日期。一旦您获得了适当分辨率的汇总余额(例如每月 3 次),您在任何特定日期的运行余额报告将非常快速,因为您只需首先获取汇总余额,加上最后一个汇总和报告日期。以上是关于连接表上的窗口函数的主要内容,如果未能解决你的问题,请参考以下文章