sql中实现移动平均必须用窗口函数avg() over (order by ... rows between ...),而非group by;需显式指定order by和rows帧,注意null处理、数据库兼容性及索引优化。

AVG函数本身不支持移动平均,必须配合窗口函数
直接对 AVG() 加 GROUP BY 只能得到分组静态均值,无法实现“每行基于前N行数据计算平均”的移动平均效果。真正在SQL里算移动平均,核心是用窗口函数 AVG() OVER (...),不是普通聚合函数。
常见错误现象:写成 SELECT AVG(value) FROM table GROUP BY date —— 这只是按天求均值,不是移动平均;或者漏写 ORDER BY 导致窗口无序、结果不可靠。
- 必须在
OVER()子句中指定ORDER BY(通常是时间字段),否则窗口范围无意义 - 推荐显式定义窗口帧,比如
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示含当前行及前2行 - 不同数据库对默认帧的处理不同:PostgreSQL 默认是
UNBOUNDED PRECEDING AND CURRENT ROW,MySQL 8.0+ 和 SQL Server 默认同理,但旧版或某些方言可能行为不一致
计算3日移动平均的典型写法(含NULL处理)
以日期字段为 trade_date、数值字段为 close_price 的股票表为例,3日移动平均即每行取自身及前2个交易日的均值:
SELECT
trade_date,
close_price,
ROUND(AVG(close_price) OVER (
ORDER BY trade_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2) AS ma3
FROM stock_daily;
注意点:
- 第1行只有1个数据,
AVG()返回它自己;第2行是前两行均值——这是正确行为,不是bug - 若某日
close_price为NULL,该行不参与计算,窗口内有效行数减少,均值基于剩余非NULL值计算 - 如果想强制补0而非跳过NULL,需先用
COALESCE(close_price, 0)转换,但业务上通常不应这么做
不同数据库对移动平均的兼容性差异
窗口函数是SQL:2003标准,但落地支持程度不一:
- MySQL:需 8.0+ 版本,低版本只能用自连接或变量模拟,极难维护且性能差
- PostgreSQL / SQL Server / Oracle / BigQuery:原生支持完整窗口语法,包括
RANGE(按值范围)和ROWS(按行数)两种帧模式 - SQLite:5.7.21+ 支持基础窗口函数,但不支持
ROWS BETWEEN语法,只能用UNBOUNDED PRECEDING或依赖RANGE - ClickHouse:支持,但语法略有不同,需用
neighbor()或runningAccumulate()替代部分场景
最稳妥的写法始终是基于 ROWS + 显式范围,避免依赖数据库对默认帧的解释。
性能关键:索引和分区能显著提速
当表有千万级数据时,AVG() OVER (ORDER BY trade_date) 可能变慢,因为窗口计算需要按序扫描。优化要点:
- 确保
ORDER BY字段(如trade_date)上有索引,最好是组合索引(trade_date, close_price) - 大宽表场景下,可先用
WHERE过滤出时间范围(如最近2年),再套窗口函数,避免全表排序 - 分区表(如按月分区)能进一步减少每次查询需扫描的数据量,尤其适合时序类移动平均需求
- 避免在
OVER()中使用表达式排序,如ORDER BY DATE_SUB(trade_date, INTERVAL 1 DAY),会导致索引失效
移动平均真正难的不是语法,而是理解窗口边界如何随每一行动态变化——这点一旦错,结果就全偏了,而且不容易肉眼发现。










