SUM OVER 累加与 ROWS BETWEEN 窗口边界:彻底搞懂滑动窗口的五种语法
在日常 SQL 数据分析中,求“累计销售总额(Running Total)”、“移动平滑均值(Moving Average)”或者“区间最大回撤”是极其高频的统计需求。
很多熟练使用SUM() OVER(PARTITION BY ... ORDER BY ...)的工程师,在被问到以下细节时往往会语塞:
- 为什么不加
ROWS BETWEEN时,如果排序列有相同的值,SUM()会把相同日期的金额一次性全加起来? ROWS BETWEEN和RANGE BETWEEN到底有什么本质区别?- 怎么用一条 SQL 算出“包含当天在内过去 7 天的动态滑动窗口销售额”?
如果对窗口边界(Window Frame Clause)的底层机制缺乏清晰认知,写出的累计指标很容易在遇到并列数据、缺失日期或海量数据聚合时产生隐蔽的统计偏差。
今天我们系统拆解OVER()子句中窗口边界定义的完整语法体系与五大实战范式。
窗口边界语法的通用骨架
一个完整的分析窗口定义由三个部分构成:
函数名() OVER ( PARTITION BY 分区字段 ORDER BY 排序字段 [ASC|DESC] [ROWS | RANGE] BETWEEN 窗口起始边界 AND 窗口结束边界 )核心关键字字典:
CURRENT ROW:当前行UNBOUNDED PRECEDING:分区内的第一行(无上界起点)UNBOUNDED FOLLOWING:分区内的最后一行(无下界终点)N PRECEDING:当前行向前偏移 $N$ 行/单位N FOLLOWING:当前行向后偏移 $N$ 行/单位
必须掌握的五大经典窗口边界模式
假设我们有一张简单的日销售明细表t_sales_daily(dt, amount)。
模式一:标准累计求和(从月初第一天累加到当天)
SELECT dt, amount, -- 语法 A:显式声明物理行边界(最安全、最推荐的标准写法) SUM(amount) OVER ( ORDER BY dt ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total_amount FROM t_sales_daily;模式二:滑动居中平均(前 1 天 + 当天 + 后 1 天,共 3 天居中窗口)
常用于时序数据平滑滤波,消除单日随机波动:
SELECT dt, amount, -- 窗口跨度:包含前 1 行、当前行、后 1 行 AVG(amount) OVER ( ORDER BY dt ASC ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS moving_avg_3d_centered FROM t_sales_daily;模式三:回溯滑动窗口(包含当天在内的过去 7 天累计)
业务场景:监控任意一天的“过去 7 天滚动总营收”:
SELECT dt, amount, -- 窗口跨度:从前 6 行到当前行(共 7 行) SUM(amount) OVER ( ORDER BY dt ASC ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS rolling_sum_7d FROM t_sales_daily;模式四:全分区全局聚合(同时获取明细与总体占比)
业务场景:在每一行明细旁边直接展示该单金额占全月大盘总金额的百分比,无需单独写子查询:
SELECT dt, amount, -- 全局求和:从分区第一行到最后一行 SUM(amount) OVER ( ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS grand_total_amount, -- 直接计算单日销售贡献占比 ROUND( amount / SUM(amount) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) * 100, 2 ) AS contribution_pct FROM t_sales_daily;模式五:前向累计(从当前天到月底最后一天,剩余指标预测)
业务场景:计算当月后续剩余天数的销售目标缺口:
SELECT dt, amount, -- 从当前行累加到分区最后一行 SUM(amount) OVER ( ORDER BY dt ASC ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING ) AS remaining_month_total FROM t_sales_daily;致命大坑:ROWSvsRANGE的暗坑差异
这是无数初学者甚至工作三年的老手都会踩翻的经典事故!
现象实测:
如果我们在写SUM(amount) OVER (ORDER BY dt)时省略了ROWS BETWEEN,SQL 标准(ANSI SQL)默认采用的缺省边界是:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(注意是RANGE而不是ROWS!)。
测试数据: 日期 (dt) | 金额 (amount) 2026-09-04 | 100 2026-09-04 | 200 <-- 存在相同日期! 2026-09-05 | 300- 如果使用
ROWS BETWEEN ...(按物理行数累加):
第 1 行结果:100
第 2 行结果:100 + 200 =300
第 3 行结果:300 + 300 = 600 - 如果不写,默认变成
RANGE BETWEEN ...(按逻辑数值范围累加):
因为第 1 行和第 2 行的dt值完全相同,RANGE认为它们属于同一个“同级数值组(Peers)”,会在这一步直接把相同日期的所有金额全加起来!
第 1 行结果:300(提前加了第 2 行!)
第 2 行结果:300
第 3 行结果:600
事故后果:如果表里有重复排序列,省略ROWS BETWEEN会导致中间行的累计值出现虚假的并列跳变,完全破坏了按行逐步递增的财务审计要求!
终极军规
- 永远显式书写
ROWS BETWEEN:在任何需要严格行级累加的 SQL 中,坚决不要依赖默认行为,完整敲出ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。 - 复用命名窗口(Window Specification):当一个查询中需要同时计算多个相同窗口维度的指标时,使用
WINDOW w AS (...)提高可读性并提升优化器执行效率:SELECT dt, SUM(amount) OVER w AS run_sum, AVG(amount) OVER w AS run_avg, MAX(amount) OVER w AS run_max FROM t_sales_daily WINDOW w AS (ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW);