MySQL 8.0 中移动平均需用 AVG() OVER (ORDER BY ... ROWS BETWEEN ...) 实现,必须显式指定确定性 ORDER BY(如 date, id),ROWS 定义物理行范围,边界自动截断,NULL 值被自动忽略,首尾不足窗口大小时默认不补 NULL。

MySQL 8.0 原生支持窗口函数,移动平均可以直接用 AVG() 配合 ROWS BETWEEN 实现,无需自连接或子查询;但边界行为、NULL 处理和排序依赖容易出错。
移动平均必须显式声明 ORDER BY 才生效
窗口函数在 MySQL 中要求 OVER() 里有 ORDER BY,否则会报错 Window 'w' with no ORDER BY is not allowed。即使业务上“顺序不重要”,也得指定一个确定性排序列(比如时间戳或自增 ID)。
实操建议:
- 用业务时间字段(如
created_at)排序最合理;若该字段有重复值,追加主键列避免排序不稳定,例如ORDER BY created_at, id - 不能用
ORDER BY RAND()或无序表达式,MySQL 不允许 - 如果原始数据没时间维度,又硬要“按插入顺序”算,需确保表有自增
id并信任其顺序
ROWS BETWEEN 的偏移写法决定计算范围
移动平均本质是滑动窗口内的均值,关键在 ROWS BETWEEN 的起点和终点描述。MySQL 不支持 RANGE 模式下的时间间隔(如 RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW),只能靠 ROWS 行数控制。
常见写法示例:
SELECT
date,
value,
AVG(value) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma_7d
FROM sales;
说明与注意点:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
-
6 PRECEDING AND CURRENT ROW是 7 日移动平均(含当天),不是 6 日 -
3 PRECEDING AND 3 FOLLOWING是中心对称的 7 日均值,适用于平滑但不预测 - 首尾几行会自动截断:第 1 行只有 1 行可算,
AVG()返回该行自身值,不是NULL - 想让开头不足窗口大小时返回
NULL,得额外用CASE WHEN ROW_NUMBER() OVER (...)
NULL 值会让 AVG() 自动跳过,但可能掩盖问题
AVG() 窗口函数默认忽略 NULL 值参与计算,这在缺失数据场景下看似友好,实则危险:你看到的 “7 日均值” 可能只基于 3 个非空值算出,且无提示。
应对策略:
- 先检查源数据中
value的NULL分布:SELECT COUNT(*), COUNT(value) FROM sales - 若需强制补零,用
COALESCE(value, 0)替代原始列,但注意 0 和缺失语义不同 - 若需识别有效样本数,叠加
COUNT(value) OVER (...)辅助列,判断是否低于阈值(如)后标记为不可信 - 别依赖
AVG(NULLIF(value, 0))这类写法来“过滤 0”,它会把真实 0 当成缺失,逻辑易错
性能敏感点:排序 + 窗口扫描无法避免,索引能帮大忙
MySQL 执行窗口函数时,必须先按 ORDER BY 排序,再逐行滑动计算。没有索引支撑的排序会触发 filesort,大数据量下极慢。
优化要点:
- 确保
ORDER BY列(如date)上有单列索引或作为联合索引最左前缀 - 如果查询还带
WHERE date >= '2024-01-01',索引应覆盖date和value,减少回表(例如INDEX(date, value)) - 避免在
OVER()中用表达式排序,如ORDER BY DATE(created_at),会导致索引失效 - 超大表慎用宽窗口(如
90 PRECEDING),虽然 MySQL 内部做了优化,但内存占用随窗口行数线性增长
真正麻烦的不是语法,而是搞清“你想平均的是哪几行”——时间对齐方式、缺失值意图、首尾截断策略,这些业务逻辑一旦定错,结果偏差比 SQL 写错更难排查。

















