avg() over() 不写 rows 默认是累积平均而非滚动平均,因其窗口默认为 range between unbounded preceding and current row;必须显式指定 rows between 2 preceding and current row 才能实现严格三行滚动平均。

直接写 AVG(profit) OVER() 只会返回全表平均值,不是滚动平均;必须显式加 ORDER BY 和 ROWS BETWEEN 才能算对。
为什么 AVG() OVER() 不写 ROWS 就不算滚动平均?
AVG() 本身是聚合函数,OVER() 只是把它“窗口化”,但窗口范围默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(从第一行到当前行),这不是固定宽度的滚动窗口。漏掉 ROWS BETWEEN,结果就是累积平均,不是你想要的“最近3期”。
- MySQL 8.0+、PostgreSQL、SQL Server 都默认用
RANGE行为,除非你明确写ROWS - 没写
ORDER BY会直接报错(如 PostgreSQL)或静默出错(如旧版 MySQL) - 即使写了
ORDER BY,若排序列有重复值(比如同一天多条记录),数据库可能退化回RANGE模式,窗口实际包含更多行
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 是怎么算的?
这是最常用、最稳定的滚动写法,适用于等间隔、无缺失的时间序列(比如每月一条记录)。它不看日期值,只数物理行:当前行 + 前两行 = 共3行参与平均。
-
2 PRECEDING表示往前取2行,CURRENT ROW是锚点,不是“从当前开始” - 首行只有自己参与计算,第二行是前1行+自己,第三行起才满3行——这是标准行为,不是 bug
- 如果原始数据是日粒度但某天没记录,
ROWS会跳过空缺,把“昨天”当成“3天前”,时间跨度就失真了 - 字段
report_month必须能字典序正确排序,比如'2024-01'✅,'2024-1'❌(会导致 10 月排在 2 月前面)
日期不连续或日粒度数据怎么办?
不能硬套 ROWS BETWEEN,得先补全时间维度,否则业务语义就错了——“最近三个月”指的是日历月,不是数据库里存在的三条记录。
- 先用递归 CTE(MySQL/SQL Server)或
GENERATE_SERIES()(PostgreSQL)生成连续月份序列 - 再
LEFT JOIN原始月度利润表,用COALESCE(profit, 0)或NULL填充空缺(填 0 还是 NULL 取决于业务含义) - 最后在补全后的完整月表上跑
AVG(profit) OVER (ORDER BY report_month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) - 如果原始是日粒度销售数据,务必先
GROUP BY DATE(order_date)或YEAR(order_date), MONTH(order_date)汇总成日/月粒度,再补日期
ORDER BY 字段没索引会影响性能吗?
影响很大。窗口函数要按排序字段重排数据,如果 ORDER BY date 没索引,大表会触发 filesort,速度可能慢几倍甚至十几倍。
- 给排序字段建索引:例如
CREATE INDEX idx_report_month ON monthly_profit(report_month) - 避免在子查询里嵌套多个
AVG() OVER,尤其在 MySQL 8.0 上——它不会复用排序结果,每个多算一次 - 想同时算 3 期、7 期、30 期?单次扫描写多个
OVER在 PostgreSQL 14+ 和 SQL Server 2022 上已优化,旧版本建议用 CTE 预先物化排序键 - 边界行和 NULL 处理是隐形坑:AVG 自动忽略 NULL,但分母是有效行数;整窗口都是 NULL 时结果为 NULL,必要时加
COALESCE(..., 0)
真正难的从来不是语法,而是确认时间字段是否严格有序、有没有重复值、空缺是否已补全、索引是否到位——这些地方一松懈,滚动平均就算对了,趋势线也已经偏了。











