连接表上的窗口函数

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 的运行平衡,但我想避免将salesreports 中的行数成比例地相乘。如果我选择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 次),您在任何特定日期的运行余额报告将非常快速,因为您只需首先获取汇总余额,加上最后一个汇总和报告日期。

以上是关于连接表上的窗口函数的主要内容,如果未能解决你的问题,请参考以下文章

从 Excel 调用 VBA 函数 - 在选定工作表上的选定列中查找

具有大量列的数据帧上的 Spark 窗口函数

Oracle 查询性能——多个复杂的窗口函数

SQL窗口函数和连接

具有基于时间的窗口的不规则时间序列上的优化滚动函数

连续 ID 块上的 PostgresQL 窗口函数