AVG() OVER必须配合ORDER BY和ROWS BETWEEN才能实现移动平均,否则默认计算全局平均值;推荐用ROWS而非RANGE以确保按物理行数稳定滑动,如3日均值写ROWS BETWEEN 2 PRECEDING AND CURRENT ROW。

AVG() OVER 里必须定义窗口范围,否则结果和普通 AVG() 没区别
直接写 AVG(price) OVER() 会算出整张表的全局平均值,不是移动平均。窗口函数要生效,必须用 ROWS BETWEEN 或 RANGE BETWEEN 明确指定“当前行往前/后看几行”。常见错误是漏掉这部分,结果看似没报错,但完全不符合移动平均的业务意图。
推荐优先用 ROWS BETWEEN:它按物理行数切片,稳定、可预期;RANGE 依赖排序列值,遇到重复值或空值容易出意外。
- 计算最近 3 天(含当天)的移动平均:用
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW - 计算过去 7 天(不含当天):用
ROWS BETWEEN 6 PRECEDING AND 1 PRECEDING - 如果日期有缺失(比如周末无数据),
ROWS仍按行数算,不是按日历天数 —— 这点必须提前确认业务是否接受
ORDER BY 是强制要求,且顺序直接影响移动窗口方向
OVER 子句里不写 ORDER BY,SQL 会报错(如 PostgreSQL、SQL Server、BigQuery 均如此)。因为移动平均必须基于某个有序序列,数据库需要知道“前 N 行”是谁。
排序字段通常选时间戳或自增 ID,但要注意:如果排序字段有重复值(例如同秒内多条订单),数据库可能任意打乱这些行的相对位置,导致同一查询多次执行结果不一致。解决方法是加一个确定性次级排序,比如 ORDER BY event_time, id。
- 升序(
ASC)是默认行为,适合“截至当前”的累计类移动平均 - 降序(
DESC)会导致窗口往未来“看”,一般不用在移动平均场景 - 没加
ORDER BY就写ROWS,多数引擎直接拒绝执行
处理 NULL 值时 AVG() 自动跳过,但窗口边界不会自动收缩
AVG() 本身对 NULL 安全 —— 它只计算非 NULL 值的平均数。但窗口范围(如 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)是固定行数,不会因为某行 price 是 NULL 就少算一行。这意味着:如果当前行和前两行里有两个 NULL,AVG() 只基于剩下的那个非 NULL 值计算,结果等于该值本身,而非 NULL。
这有时符合预期(比如你想忽略异常缺失值),但若业务要求“必须有足够有效样本才计算”,就得额外加判断:
AVG(price) OVER ( ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS moving_avg, COUNT(price) OVER ( ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS valid_count
然后在应用层或外层 WHERE valid_count >= 3 过滤。
性能敏感场景下,避免在大结果集上反复重算窗口
窗口函数本质是逐行扫描+维护滑动状态,当分区大、排序字段无索引、或同时跑多个窗口(比如同时算 5/10/20 日均线)时,执行变慢很常见。优化关键点就两个:
- 确保
ORDER BY字段上有索引(如CREATE INDEX idx_ts ON sales(ts)) - 如果只需要最新几条移动平均值,别先算全表再
LIMIT,改用子查询或 CTE 配合ROW_NUMBER()截断数据源 - MySQL 8.0+ 支持窗口函数,但旧版本(如 5.7)不支持,得用自连接或变量模拟,逻辑复杂且难维护
移动平均看着简单,真正落地时,排序稳定性、NULL 处理粒度、以及窗口边界的语义理解,才是最容易出问题的地方。

















