SQL移动平均必须用AVG() + OVER() + ROWS BETWEEN,如ROWS BETWEEN 2 PRECEDING AND CURRENT ROW计算最近3行均值;必须显式ORDER BY确保行序,禁用RANGE避免同日期多行误聚合,缺失日期需先左连接日历表补全。

SQL移动平均必须用窗口函数,AVG() + OVER() 是唯一可靠解法
直接写 AVG(sales) 不会按时间滚动计算,必须配合窗口子句。核心在于 ROWS BETWEEN N-1 PRECEDING AND CURRENT ROW —— 这个范围定义决定了“最近N天”是向前取N行(含当前行),不是按日期字段自动对齐。
常见错误是写成 RANGE BETWEEN INTERVAL '2' DAY PRECEDING AND CURRENT ROW,但 PostgreSQL 以外多数数据库(MySQL 8.0、SQL Server)不支持 RANGE 配合时间间隔,且即使支持,RANGE 会合并同日期多行,导致结果偏高或跳变。
- 确保销售数据已按日期升序排序:
ORDER BY sale_date必须显式写出,否则窗口帧无意义 - 若存在某天无销售记录,
ROWS模式不会补零,只会少算天数;需先用日历表LEFT JOIN补全日期 - MySQL 8.0+ 和 PostgreSQL 支持该语法;SQLite 3.25+ 支持但性能较差;旧版 MySQL(
ROWS BETWEEN 和 RANGE BETWEEN 的实际差异在哪
假设每天有多笔订单,sale_date 相同但 id 不同:
2024-01-01 | 100 2024-01-01 | 150 2024-01-02 | 200 2024-01-03 | 300
用 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 计算3日移动平均时,第3行(2024-01-02)的窗口包含前两行 + 自身 → 共3行;而 RANGE BETWEEN INTERVAL '2' DAY PRECEDING AND CURRENT ROW 会把 2024-01-01 的两行都纳入,共4行参与平均。
-
ROWS数的是物理行数,稳定可控,推荐用于“最近N条记录”场景 -
RANGE数的是逻辑值范围,适合“最近N天内所有数据”,但依赖数据库对INTERVAL的支持,且易受重复日期影响 - SQL Server 完全不支持
RANGE配合DATE类型,只能用ROWS+ROW_NUMBER()间接模拟
如何处理日期不连续导致的移动平均断层
真实销售数据常缺某些日期(如周末无交易),直接用 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 会把“最近3条记录”当成“最近3天”,造成时间跨度失真。例如:1月1日、1月4日、1月5日有数据,第三行的窗口实际覆盖了5天而非3天。
- 正确做法是先生成连续日期序列(用
GENERATE_SERIES()(PostgreSQL)、RECURSIVE CTE(MySQL/SQL Server)或临时日历表) - 再用
LEFT JOIN补全销售数据,缺失日销售额填0或NULL(注意AVG()会忽略NULL) - 最后在补全后的结果集上应用窗口函数,此时
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW才真正对应“最近3个自然日” - 如果业务允许近似,也可用
LAG()分别取前1、2行再手工平均,但超过5天就难维护
MySQL 8.0 中 AVG() OVER() 的典型写法和坑点
MySQL 8.0 支持标准窗口语法,但默认排序行为容易被忽略:
SELECT sale_date, sales, AVG(sales) OVER (ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS ma3 FROM sales;
- 漏写
ORDER BY会导致窗口帧不可预测(MySQL 可能按插入顺序,但不保证) -
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示“自身 + 前2行”,共3行 → 对应3日平均;要N日就写N-1 PRECEDING - 若首两行无法凑满N行,MySQL 返回非空子集的平均值(如第1行只算自己),无需额外
CASE WHEN判断 - 性能上,窗口函数比自连接快一个数量级,但数据量超千万时仍建议在
sale_date上建索引
移动平均真正的复杂点不在语法,而在日期对齐逻辑——到底是按记录序号滑动,还是按自然日历滑动。选错模式,结果可能偏差30%以上,而且很难一眼看出来。










