MySQL 8.0 的 AVG() 窗口函数需显式指定 ROWS 范围才能实现移动平均,仅 ORDER BY 会导致全分区平均;ROWS 必须配合确定且无重复的 ORDER BY,否则结果不稳定或报错。

MySQL 8.0 的 AVG() 窗口函数必须搭配 ROWS 才能算移动平均
MySQL 8.0 支持窗口函数,但 AVG() 默认不回溯历史行——你得显式用 ROWS BETWEEN ... AND ... 定义滑动窗口范围,否则就是全分区平均(等价于 AVG() OVER (PARTITION BY ...)),不是移动平均。
常见错误是只写 AVG(x) OVER (ORDER BY t),结果每行都得到整个有序分区的平均值,完全没“移动”起来。
-
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW:当前行 + 前两行,共 3 期移动平均 -
ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING:当前行 + 后两行(适用于预测类平滑) -
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING:前后各一期,中心对称,常用于降噪
ORDER BY 是 ROWS 范围生效的前提,且必须确定、无重复
没有 ORDER BY,ROWS 子句直接报错:ERROR 3586 (HY000): Window '<name>' with RANGE or ROWS frame requires an ORDER BY clause</name>。更隐蔽的问题是:如果 ORDER BY 字段存在重复值(比如多条记录时间戳相同),MySQL 会任意排序这些行,导致同一组数据每次执行结果不一致,移动平均值漂移。
- 务必用唯一排序键,例如
ORDER BY created_at, id(id补充去重) - 避免仅用
ORDER BY date,尤其当单日有多条记录时 - 若原始数据无天然顺序,可先加
ROW_NUMBER() OVER ()生成序号再排序
计算 5 日移动平均的典型写法与性能注意点
假设表 stock_price 有字段 trade_date(DATE 类型)、close_price(DECIMAL),想按交易日顺序算 5 日收盘价均值:
SELECT
trade_date,
close_price,
AVG(close_price) OVER (
ORDER BY trade_date, id
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) AS ma5
FROM stock_price
ORDER BY trade_date;
注意几个实际坑:
- MySQL 不支持
ROWS BETWEEN INTERVAL 4 DAY PRECEDING这种时间偏移写法,必须基于行数,不是天数——如果中间有休市日,4 PRECEDING可能跳过真实 5 个交易日 - 首 4 行结果为
NULL或部分行均值(取决于是否含CURRENT ROW),需用COALESCE(ma5, close_price)填补 - 大表上加
ORDER BY ... ROWS会触发临时文件排序,建议在(trade_date, id)上建联合索引
替代方案:用自连接或子查询模拟移动平均的适用场景
当需要严格按自然日(而非行数)计算、或 MySQL 版本低于 8.0 时,窗口函数不可用,就得绕路。但即使在 8.0,遇到“最近 5 个非空交易日”这类逻辑,ROWS 也无能为力,因为无法跳过休市/缺失数据行。
这时必须用相关子查询:
SELECT
t1.trade_date,
t1.close_price,
(SELECT AVG(t2.close_price)
FROM stock_price t2
WHERE t2.trade_date <= t1.trade_date
AND t2.trade_date >= (
SELECT MAX(t3.trade_date)
FROM stock_price t3
WHERE t3.trade_date < t1.trade_date
ORDER BY t3.trade_date DESC
LIMIT 4 OFFSET 0
)
) AS ma5_calendar
FROM stock_price t1;
这种写法代价高,但逻辑可控;而 ROWS 方案快且简洁,前提是你的“移动”定义恰好匹配行序。
真正难的从来不是写对语法,而是想清楚:你要平均的到底是“最近 N 行”,还是“最近 N 个业务周期”。前者交给 ROWS,后者得自己兜底。


















