移动平均与累计曲线:窗口框架

增长要「7 日滑动 GMV」「累计 GMV 曲线」。你发现同样的 sum() OVER (),写不写 ORDER BY 结果完全不同。

学 45 min
练 60 min
盘 15 min
共 120 分钟

学 · 45 min

  1. 01默认框架:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

    场景同一个 sum() over (...),写不写 order by 结果天差地别--先搞清「框架」是什么。

    窗口函数的完整语法是 over (partition by ... order by ... 框架)。框架(frame)回答:每一行的计算范围到哪里为止。有 ORDER BY 时默认 = 从组头到当前行(所以 sum 变成累计和);没 ORDER BY 时默认 = 整组(所以 sum 是组内总和)。

    select 月份, gmv,
           sum(gmv) over (order by 月份) as 累计,      -- 到当前行为止
           sum(gmv) over ()                 as 总和    -- 整组(全表)
    from (select date_trunc('month', created_at) as 月份, sum(total_amount) as gmv
          from orders group by 1) t;

    易错「不小心多写了 order by,sum 变累计」是本周最容易犯的错--写聚合窗口前先想清楚要不要框架。

  2. 02ROWS(按行数)vs RANGE(按值)vs GROUPS

    场景算 7 日移动平均写了 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW--这个 ROWS 是什么,能换成 RANGE 吗?

    ROWS 按物理行数划范围(往上看 6 行);RANGE 按排序值划范围(排序值和当前行相同/之前的都算,并列的行一起进来);GROUPS 按「同值行组」计数。排序列没有重复值时 ROWS 和 RANGE 结果相同;一旦有并列(同一天多行),两者结果就分叉。

    -- 造一组带重复时间戳的数据,对比两种框架
    select d, v,
           sum(v) over (order by d rows   between 1 preceding and current row) as 前一行,
           sum(v) over (order by d range  between interval '1 day' preceding and current row) as 按值
    from (values (timestamp '2026-01-01 10:00', 10), (timestamp '2026-01-01 12:00', 20),
                 (timestamp '2026-01-02 09:00', 30), (timestamp '2026-01-03 08:00', 40)) t(d, v);

    易错排序列有重复值时 RANGE 会把「同值的所有行」拉进范围,数字悄悄变大--又一个报表口径事故源。

  3. 03UNBOUNDED PRECEDING / N PRECEDING / CURRENT ROW / FOLLOWING

    场景「7 日移动平均」「近 30 日累计」--把框架边界这几个词玩熟。

    框架边界四个词:unbounded preceding 组头、N preceding 往上 N 行、current row 当前行、N following / unbounded following 往下 N 行 / 组尾。between 两头都含。7 日移动平均 = rows between 6 preceding and current row(含当前共 7 行)。

    select 日期, gmv,
           round(avg(gmv) over (order by 日期
                 rows between 6 preceding and current row), 0) as 七日均值,
           sum(gmv)  over (order by 日期) as 累计
    from (select created_at::date as 日期, sum(total_amount) as gmv
          from orders group by 1) t;

    易错写 6 preceding 不是 7 preceding--between 是闭区间,含当前行一共 7 行。

  4. 04有 ORDER BY 和没有 ORDER BY 时默认框架不同

    场景昨天 D24 第 5 题的坑,今天正式收掉。

    汇总:无 ORDER BY -> 整组;有 ORDER BY -> 组头到当前行(RANGE 语义)。所以 last_value 想取「真正的最后一行」,必须把框架撑满:rows between unbounded preceding and unbounded following

    select user_id, created_at, total_amount,
           last_value(total_amount) over (
             partition by user_id order by created_at
             rows between unbounded preceding and unbounded following   -- 撑到组尾
           ) as 最后一单金额
    from orders;

    易错另一条路:first_value + order by 倒排(desc),取「排序后的第一行」= 最后一行--不用记长框架,语义也更直白。

练 · 60 min

  1. 算 7 日移动平均 GMV(ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    参考答案

    between 是闭区间:6 preceding + 当前行 = 7 行,别写成 7 preceding。头 6 天窗口不足 7 行,是「有多少算多少」的均值,要严格 7 天得另加行数校验。

    select 日期, gmv,
           round(avg(gmv) over (order by 日期
                 rows between 6 preceding and current row), 0) as 七日均值
    from (select created_at::date as 日期, sum(total_amount) as gmv
          from orders group by 1) t
    order by 日期;
  2. 算每日累计 GMV
    参考答案

    有 ORDER BY 时默认框架 = 组头到当前行,sum 自动变累计。这就是 D22 第 5 题 sum(1) 是「累计条数」的原因。

    select 日期, gmv,
           sum(gmv) over (order by 日期) as 累计gmv
    from (select created_at::date as 日期, sum(total_amount) as gmv
          from orders group by 1) t
    order by 日期;
  3. 造一组有重复排序值的数据,对比 ROWS 与 RANGE 的结果差异
    参考答案

    前两行 d 相同(并列):RANGE 把同值行一起拉进范围,第一行按值算出 10+20=30、第三行算出 10+20+30=60;ROWS 只看上一行得 10 和 50。排序列有重复值时两者分叉,是报表口径事故源。细节:RANGE 的偏移对 date 列只认 interval,所以要造 timestamp 的小表。

    select d, v,
           sum(v) over (order by d rows  between 1 preceding and current row) as rows前一行,
           sum(v) over (order by d range between interval '1 day' preceding and current row) as range按值
    from (values (timestamp '2026-01-01 00:00', 10), (timestamp '2026-01-01 00:00', 20),
                 (timestamp '2026-01-02 00:00', 30), (timestamp '2026-01-03 00:00', 40)) t(d, v);
    -- rows前一行: 10 / 30 / 50 / 70      range按值: 30 / 30 / 60 / 70
  4. 去掉 ORDER BY 再跑一次累计求和,解释结果为什么变了
    参考答案

    无 ORDER BY 时默认框架 = 整组(这里整表):每行都贴同一个全表总和,累计语义消失。「多写 / 少写一个 order by」是窗口聚合最常见的口径事故。

    select 日期, gmv,
           sum(gmv) over () as 全表总和
    from (select created_at::date as 日期, sum(total_amount) as gmv
          from orders group by 1) t
    order by 日期;
  5. 用正确的框架修正 D24 第 5 题的 last_value
    参考答案

    框架撑满组内全部行后,last_value 才取到真正的最后一行。另一条路:first_value + order by created_at desc 倒排取第一行,不用背长框架、语义更直白。

    select user_id, created_at, total_amount,
           last_value(total_amount) over (
             partition by user_id order by created_at
             rows between unbounded preceding and unbounded following   -- 框架撑到组尾
           ) as 最后一单金额
    from orders
    limit 20;
过关标准 说出默认框架是什么、在什么数据下会给出反直觉的结果。