【问题标题】:How to efficiently compute rolling sum over multiple dates, grouping by id?如何有效地计算多个日期的滚动总和,按 id 分组?
【发布时间】:2021-12-19 17:42:05
【问题描述】:

给定一张表,每天包含 1 行 dtproduct_id 和当天生成的 turnover,如何有效地计算包含该产品产生的营业额的列 turnover_7day过去 7 天?

我发现一个简单的查询按预期工作,但速度很慢,我正试图在几年内对数百万种产品运行查询。

SQL Fiddle
(即使 Fiddle 是 Postgresql,IRL 我正在尝试在 Snowflake 上执行此操作;我怀疑 Snowflake 中是否有可以完全改变这篇文章的潜在答案的功能)

数据集:

TABLE turnover_per_day:
| product_id | product_name |         dt | turnover |
|------------|--------------|------------|----------|
|          1 |          PS5 | 2021-10-22 |       85 |
|          1 |          PS5 | 2021-10-27 |      100 |
|          1 |          PS5 | 2021-11-01 |      110 |
|          1 |          PS5 | 2021-11-05 |      150 |
|          2 |         XBOX | 2021-11-02 |       10 |
|          2 |         XBOX | 2021-11-03 |       15 |
|          2 |         XBOX | 2021-11-04 |       13 |
|          2 |         XBOX | 2021-11-05 |       11 |

方法一:SELECT语句中的子查询(产生预期的结果,效率很低):

我在这里使用源表的子查询来重新计算营业额。看起来确实效率很低,但至少它很容易理解。

查询:

SELECT
    t1.product_id
    ,t1.product_name
    ,t1.turnover
    ,t1.dt
    ,(
        SELECT SUM(turnover) FROM turnover_per_day t2
        WHERE (t2.dt BETWEEN t1.dt - interval '6 day' AND t1.dt) and t1.product_id=t2.product_id
    ) as turnover_7day
FROM turnover_per_day as t1
order by product_id, t1.dt

结果(如预期):

| product_id | product_name | turnover |         dt | turnover_7day |
|------------|--------------|----------|------------|---------------|
|          1 |          PS5 |       85 | 2021-10-22 |            85 |
|          1 |          PS5 |      100 | 2021-10-27 |           185 |
|          1 |          PS5 |      110 | 2021-11-01 |           210 |
|          1 |          PS5 |      150 | 2021-11-05 |           260 |
|          2 |         XBOX |       10 | 2021-11-02 |            10 |
|          2 |         XBOX |       15 | 2021-11-03 |            25 |
|          2 |         XBOX |       13 | 2021-11-04 |            38 |
|          2 |         XBOX |       11 | 2021-11-05 |            49 |

方法2:尝试重现this answer(但失败)

这里我尝试使用窗口函数来加快计算速度。 我试图在上面链接的答案中添加PARTITION BY product_id,但它没有按预期工作。 我的想法是因为product_id 列的LEFT JOIN 值是NULL,并且在取前 6 行时,它会“删除”NULL 行,因此总和超过 6 天。

查询:

with days as ( -- generate a calendar without gap 
  SELECT date_trunc('day', d)::date as day
  FROM generate_series(CURRENT_DATE-15, CURRENT_DATE, '1 day'::interval) d
 )
select
    days.day
    ,t1.product_id
    ,t1.product_name
    ,t1.turnover
    ,t1.dt
    ,SUM(t1.turnover) OVER (PARTITION BY t1.product_id ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS turnover_7day

FROM days
LEFT JOIN turnover_per_day as t1
ON days.day = t1.dt
--where t1.product_id is not null
order by product_id, dt

结果(错误):

|        day | product_id | product_name | turnover |         dt | turnover_7day |
|------------|------------|--------------|----------|------------|---------------|
| 2021-10-22 |          1 |          PS5 |       85 | 2021-10-22 |            85 |
| 2021-10-27 |          1 |          PS5 |      100 | 2021-10-27 |           185 |
| 2021-11-01 |          1 |          PS5 |      110 | 2021-11-01 |           295 |
| 2021-11-05 |          1 |          PS5 |      150 | 2021-11-05 |           445 |
| 2021-11-02 |          2 |         XBOX |       10 | 2021-11-02 |            10 |
| 2021-11-03 |          2 |         XBOX |       15 | 2021-11-03 |            25 |
| 2021-11-04 |          2 |         XBOX |       13 | 2021-11-04 |            38 |
| 2021-11-05 |          2 |         XBOX |       11 | 2021-11-05 |            49 |
| 2021-10-31 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-29 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-23 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-24 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-25 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-26 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-28 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-21 |     (null) |       (null) |   (null) |     (null) |        (null) |
| 2021-10-30 |     (null) |       (null) |   (null) |     (null) |        (null) |

我的问题是:

  1. 如何修改方法2以使其工作?
  2. 是否还有其他数据/计算密集度较低的方法来计算此滚动总和(按 product_id 分组)?

【问题讨论】:

    标签: sql rolling-sum


    【解决方案1】:

    您需要使用以 ORDER BY 列为单位计数的 RANGE PRECEDING,而不是使用以行数计算的 ROWS PRECEDING。

    select
        t1.product_id
        ,t1.product_name
        ,t1.turnover
        ,t1.dt
        ,SUM(t1.turnover) OVER (PARTITION BY t1.product_id ORDER BY dt RANGE BETWEEN '6 days' PRECEDING AND CURRENT ROW) AS turnover_7day
    FROM turnover_per_day as t1
    order by product_id, dt;
    

    如果您为每个产品创建密集的天数系列,则可以使 ROW PRECEDING 方法起作用,而您在代码中没有这样做。但我怀疑它会表现良好。

    【讨论】:

    • Snowflake 和 PostgreSQL 都没有为滚动窗口函数实现 RANGE BETWEEN docs.snowflake.com/en/sql-reference/…
    • 我在发布之前在 PostgreSQL 中对其进行了测试,它在那里工作。在 v11 中实现,看起来像。我不知道雪花。您的第一次尝试还使用正确的索引提供了合理的性能(同样,在 PostgreSQL 中——我仍然不知道雪花)
    猜你喜欢
    • 1970-01-01
    • 2013-11-10
    • 1970-01-01
    • 2019-12-11
    • 2022-01-13
    • 2015-06-01
    • 2021-05-27
    • 2022-01-28
    • 2016-09-28
    相关资源
    最近更新 更多