sql中直接用avg()窗口函数配合rows between子句即可实现移动平均,需显式order by确保行序稳定,如rows between 2 preceding and current row计算含当前行共3行的均值,且必须避免排序字段重复导致窗口错位。

SQL里直接用AVG()窗口函数加ROWS BETWEEN就能算移动平均
标准SQL(PostgreSQL、SQL Server、Oracle、BigQuery、MySQL 8.0+)都支持AVG()作为窗口函数,配合ROWS BETWEEN子句定义滑动窗口范围。关键不是写GROUP BY,而是先按时间/序号排序,再开窗——GROUP BY是后续聚合维度,和移动平均本身不冲突。
常见错误是试图在GROUP BY后套用AVG(),结果得到的是分组内静态均值,不是“每行往前看N期”的移动平均。正确做法是:先用ORDER BY确定序列顺序,再用ROWS BETWEEN N PRECEDING AND CURRENT ROW限定窗口。
- 必须有明确的排序字段(比如
order_date或id),否则ROWS BETWEEN行为不可靠 - 如果排序字段有重复值,建议加二级排序(如
ORDER BY order_date, id)避免非确定性结果 - MySQL 5.7及更早版本不支持窗口函数,强行用会报错
ERROR 1064
GROUP BY和移动平均要分两层处理:先开窗,再分组聚合
想按产品类别看每个类别的销售移动平均?不能把GROUP BY category和窗口函数混在同一层SELECT里——窗口函数在GROUP BY之后执行,但它的计算依赖原始行粒度。正确结构是:子查询或CTE中先算出每行的移动平均,外层再按category聚合(比如取每个类别的最新移动均值、或平均移动均值)。
例如,要获取每个category下最近3天销售额的移动平均(按日期滚动),得这样写:
SELECT
category,
AVG(ma_3d) AS avg_of_ma_per_category
FROM (
SELECT
category,
sale_amount,
AVG(sale_amount) OVER (
PARTITION BY category
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS ma_3d
FROM sales
) t
GROUP BY category;
-
PARTITION BY category确保窗口只在同类数据内滑动,不会跨类别“借数” - 外层
GROUP BY是对已算好的移动平均值做二次统计,不是重算移动平均 - 如果漏掉
PARTITION BY,所有类别的数据会混在一起排序滑动,结果完全失真
SQLite和旧版MySQL用户得用自连接或子查询模拟窗口
SQLite直到3.25.0才支持窗口函数,且ROWS BETWEEN语法受限;MySQL 5.7及之前版本只能靠关联查询硬凑。性能差、写法绕,但能用。
以MySQL 5.7为例,算每个id往前2行的移动平均(假设表按id自然递增):
SELECT
t1.id,
t1.value,
(t1.value + COALESCE(t2.value, 0) + COALESCE(t3.value, 0)) /
(1 + IF(t2.value IS NOT NULL, 1, 0) + IF(t3.value IS NOT NULL, 1, 0)) AS ma_3
FROM data t1
LEFT JOIN data t2 ON t2.id = t1.id - 1
LEFT JOIN data t3 ON t3.id = t1.id - 2
ORDER BY t1.id;
- 必须保证
id连续且无缺失,否则t2.id = t1.id - 1会跳过空缺位置 - 用
COALESCE和动态计数避免除零,但逻辑易错,尤其边界行(前两行)结果可能不准 - 数据量稍大(>10万行)时,多表JOIN会明显变慢,别在生产环境硬扛
NULL值和边界行的处理最容易被忽略
移动平均遇到开头几行或NULL值时,默认行为未必符合预期:有些数据库对含NULL的窗口仍参与计数(导致分母偏大),有些则跳过NULL但不调整分母。结果可能偏小或报错。
- 显式过滤NULL:
WHERE value IS NOT NULL再开窗,最稳妥 - 用
IGNORE NULLS(PostgreSQL 15+/BigQuery支持)让窗口自动跳过NULL,但注意不是所有数据库都支持该修饰符 - 首行的移动平均默认只有自己,第二行是前两行均值……这个“自然收缩”是正常行为,不必强行补0,否则扭曲趋势
- 如果业务要求固定窗口长度(比如必须3个数,不足就返回NULL),得加
CASE WHEN ROW_NUMBER() OVER (...)
移动平均的核心是序列意识——它天然依赖顺序和邻近性。GROUP BY只是给这个序列打标签,不是替代排序。没排好序就加PARTITION BY,等于在乱序数据上强行滑动,结果连调试都难定位。











