移动平均线是每行基于滑动窗口(如前4行+当前行)计算的均值,AVG()单独使用会压缩行数,无法逐行输出结果;必须用AVG() OVER (ORDER BY ... ROWS BETWEEN 4 PRECEDING AND CURRENT ROW)实现。

什么是移动平均线,为什么不能直接用 AVG() 函数
移动平均线(Moving Average)本质是窗口内连续 N 条记录的均值,且每条记录对应一个“以它为终点”的滑动窗口。SQL 的 AVG() 是聚合函数,单独使用会压缩行数,无法保留原始时间序列的每一行结果——你不是要“算一个平均值”,而是要“给每一行算一个平均值”。所以必须借助支持行级窗口计算的能力,而非传统子查询。
真正可行的方式是窗口函数 AVG() OVER ();嵌套子查询强行实现移动平均不仅低效,还极易出错。
用窗口函数写 5 日移动平均(标准写法)
假设表 stock_prices 有字段 date、close_price,且数据已按日期升序排列:
SELECT
date,
close_price,
AVG(close_price) OVER (
ORDER BY date
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) AS ma_5
FROM stock_prices;关键点:
-
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW表示包含当前行和它前面 4 行,共 5 行 → 实现“5 日” - 必须带
ORDER BY date,否则窗口无序,PRECEDING无意义 - 若某天缺失(如节假日),该行不会被跳过,但窗口仍按物理行数取 —— 若需严格按日历天数对齐,得先生成完整日期序列再 LEFT JOIN 补空
嵌套子查询硬凑移动平均的典型错误
有人尝试这样写:
SELECT
t1.date,
t1.close_price,
(SELECT AVG(t2.close_price)
FROM stock_prices t2
WHERE t2.date <= t1.date
AND t2.date >= DATE_SUB(t1.date, INTERVAL 4 DAY)) AS ma_5_bad
FROM stock_prices t1;问题很多:
- 日期字段若含时分秒,
DATE_SUB可能漏掉同一天多条记录;用BETWEEN或>=/更稳妥 - 若存在重复日期或乱序数据,子查询返回行数不可控,
AVG()结果失真 - 性能极差:外层每行都触发一次全表扫描(或索引范围扫描),O(n²) 复杂度,万级数据就明显卡顿
- MySQL 5.7 或更早版本不支持相关子查询中引用外层的
ORDER BY,结果顺序无法保证
兼容旧版 MySQL(无窗口函数)的折中方案
如果数据库是 MySQL 5.6 或某些只读 OLAP 系统不支持窗口函数,可用变量模拟,但仅限单用户、单线程、有序查询场景:
SELECT
date,
close_price,
@ma := (@sum := @sum + close_price - IF(@cnt < 5, 0, @prev5)) /
(@cnt := IF(@cnt < 5, @cnt + 1, 5)) AS ma_5
FROM stock_prices
CROSS JOIN (SELECT @sum := 0, @cnt := 0, @prev5 := 0) AS _
ORDER BY date;这个写法脆弱点明显:
- 必须确保
ORDER BY date生效,否则变量累积错位 - 不能并行执行,不能在视图或子查询中复用
- @prev5 需精确记住第 i−5 行的值,一旦中间有 NULL 或计算中断,后续全错
- SQL 标准不保证变量赋值顺序,MySQL 8.0+ 已明确不推荐此用法
实际项目中,宁可导出数据用 Python/Pandas 算,也别在 SQL 里硬扛这种逻辑。

















