SQL移动平均必须用窗口函数而非AVG()加GROUP BY,因后者会丢失行顺序;需以ORDER BY确保排序稳定,用ROWS BETWEEN定义滑动窗口,如ROWS BETWEEN 2 PRECEDING AND CURRENT ROW实现3期移动平均,并注意数据类型、NULL及数据库兼容性。

SQL移动平均为什么不能直接用AVG()加GROUP BY
因为移动平均要求每个结果行对应一个“滑动窗口内的均值”,而GROUP BY会折叠行、丢失原始时间/顺序维度。必须用窗口函数,否则算出来的是分组总均值,不是逐行滚动的均值。
关键前提是:数据必须有明确的排序依据(如order_date、id等),否则ROWS子句无法定义“前N行”或“前后N行”。
- 没
ORDER BY子句的窗口函数行为未定义,多数数据库会报错或返回不可靠结果 -
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示包含当前行及它前面2行,共3个点的平均值 - 若排序列存在重复值,需额外加唯一列(如
id)避免排序不稳定
如何写一个标准的3期移动平均(含边界处理)
以销售表sales为例,按日期计算3天移动平均销售额:
SELECT
order_date,
amount,
AVG(amount) OVER (
ORDER BY order_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3d
FROM sales;
这个写法在首两行会自动“截断”窗口:第1行只有1个值参与计算,第2行有2个,第3行起才满3个。这是ROWS的默认行为,无需额外CASE判断。
- 想强制只显示满窗结果?加
WHERE ROW_NUMBER() OVER (ORDER BY order_date) >= 3 - 想用“前1后1”对称窗口?改用
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING - PostgreSQL和SQL Server支持
RANGE,但对时间字段易出错(相同日期会被合并),优先用ROWS
常见错误:ORDER BY列类型不匹配或NULL导致窗口断裂
如果order_date是TEXT型存"2023-01-01",排序会按字符串字典序,"2023-10-01"会排在"2023-2-01"前面——结果完全错乱。
- 务必确认排序列是
DATE、TIMESTAMP或严格递增的数值型(如自增id) -
NULL值在ORDER BY中默认排最前(MySQL)或最后(PostgreSQL),会导致窗口包含意外空值;建议提前过滤:WHERE order_date IS NOT NULL - SQLite不支持
ROWS子句(直到3.28+才部分支持),用它会报near "ROWS": syntax error,得换用子查询模拟
性能注意:窗口函数在大数据量下可能变慢
移动平均本质是每行都扫一遍窗口范围,当窗口很大(如ROWS BETWEEN 99 PRECEDING AND CURRENT ROW)且数据超百万行时,执行计划可能退化为嵌套循环。
- 加复合索引:
CREATE INDEX idx_sales_orderdate_amount ON sales(order_date, amount)能显著加速 - 避免在
SELECT里混用多个不同窗口(如同时算5期、10期、20期),每个都会独立计算;可考虑用CTE预排好序 - ClickHouse、Doris等OLAP数据库对窗口函数做了深度优化,但MySQL 8.0对大窗口仍较吃力
真正难的不是写出语法,而是确认排序键是否稳定、是否覆盖所有业务场景中的时间缝隙(比如节假日无数据,窗口会不会跳过?要不要用GENERATE_SERIES补全日期?这些得结合业务定,不是SQL一句能解决的。











