滚动平均必须用AVG()配合OVER()窗口函数实现,不可用GROUP BY;关键在ROWS BETWEEN帧定义,需确保日期有序、索引优化,并显式处理NULL与边界行。

移动平均必须用 AVG() 配合 OVER(),不能用 GROUP BY
很多人写滚动平均时第一反应是先 GROUP BY 时间字段再算平均,结果发现不是“滚动”,而是按天/月分组的静态均值。窗口函数才是唯一正解:它不压缩行数,每行都能基于前后若干行动态计算。
关键在 OVER() 里的帧定义——漏掉 ROWS BETWEEN 或写错范围,出来的就是全表平均或报错。
-
AVG(price) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW):包含当前行及前两行,共3期移动平均 - 如果省略
ROWS BETWEEN,默认是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(累积平均,不是滚动) - 日期字段必须严格有序且无重复;若存在同一天多条记录,需额外加
ROW_NUMBER()或用RANGE配合INTERVAL(PostgreSQL 支持,MySQL 8.0+ 不支持)
MySQL 8.0+ 和 PostgreSQL 的 ROWS 行为基本一致,但 SQL Server 要小心 UNBOUNDED 写法
三者都支持标准窗口语法,但 SQL Server 对 UNBOUNDED FOLLOWING 的解析更严格,尤其在子查询嵌套时容易报 Windowed functions can only appear in the SELECT or ORDER BY clauses。MySQL 8.0+ 和 PostgreSQL 在处理空值上也略有差异:NULL 值默认被忽略(符合 SQL 标准),但如果你的业务要求把 NULL 当 0 算,就得先 COALESCE(price, 0)。
- MySQL 和 PostgreSQL 中,
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING是合法的;SQL Server 要求必须以CURRENT ROW为锚点,不支持直接写1 FOLLOWING - PostgreSQL 支持
RANGE BETWEEN '7 days' PRECEDING AND CURRENT ROW(时间范围),MySQL 目前只认行数,不认时间间隔 - 所有数据库中,
ORDER BY子句在OVER()里不可省略——哪怕你只是想按插入顺序滚动,也得有明确排序依据
性能瓶颈常出在未建索引的 ORDER BY 字段和大偏移量
窗口函数本身不慢,但数据库要为每一行构造滑动窗口,如果 ORDER BY 字段没索引,就会触发全表排序;更隐蔽的问题是当你要算 90 天移动平均,而数据有上亿行时,即使有索引,ROWS BETWEEN 89 PRECEDING AND CURRENT ROW 也会让优化器放弃高效算法,退化成逐行扫描。
- 确保
ORDER BY字段(如date、created_at)上有 B-tree 索引 - 避免在子查询里嵌套多层窗口函数,尤其是带不同
ORDER BY的;先用 CTE 提前排好序 - 如果滚动周期固定且很大(比如年均值),考虑预计算中间表 + 定时任务更新,而不是每次查都实时算
别忽略 NULL 值和边界行的默认行为
第一行没有“前两行”,所以 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 在开头会返回 AVG of 1 或 2 行——这是对的,不是 bug。但如果你的应用代码假设每行都有完整窗口结果,就会出逻辑错误。同样,遇到 NULL 值时,AVG() 默认跳过它,不会报错也不会警告,可能让你误以为数据完整。
- 首行结果取决于实际可用行数:第 1 行 → 1 行均值,第 2 行 → 2 行均值,第 3 行起才稳定为 3 行均值
- 用
COUNT(*) OVER (...)同步检查当前窗口有效行数,便于前端判断是否展示“数据不足”提示 - 显式处理
NULL:比如AVG(COALESCE(sales, 0)) OVER (...),但要注意这改变了统计意义——是“把缺数当 0 算”,不是“忽略缺数”

















