AVG()配合ROWS BETWEEN是计算移动平均值的正确方法,本质是对当前行及前后指定行数做动态平均,必须用窗口函数而非GROUP BY;如3日平均需写ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING,且必须显式ORDER BY确保排序稳定,避免RANGE因重复值导致窗口漂移。

用 AVG() 配合 ROWS BETWEEN 定义窗口范围
移动平均值本质是「对当前行及前后若干行」做平均,不能靠 GROUP BY,必须用窗口函数限定动态行集。核心是用 ROWS BETWEEN 明确滑动窗口的上下界,比如 3 日移动平均就是「当前行前 1 行 + 当前行 + 后 1 行」。
常见错误是写成 RANGE BETWEEN —— 它按值分组,遇到重复时间或相同排序值会意外扩大窗口,导致结果偏高或重复计算。
-
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING:标准 3 行移动平均(含自身) -
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW:包含当前行的 3 日累积平均(常用在“截至今日的最近3天”场景) - 排序字段必须明确且稳定,否则
ORDER BY在OVER子句中缺失或使用无序列(如未加id或date),结果不可预测
处理边界行时 AVG() 自动截断,但要注意 NULL 影响
窗口在开头或结尾无法凑满指定行数时,ROWS BETWEEN 会自动收缩到可用行,AVG() 仍正常计算非 NULL 值的平均——这点和 Excel 不同,Excel 移动平均常强制补 0 或报错,而 SQL 默认忽略 NULL。
但如果业务要求首尾补 0 或保留 NULL,就得显式处理:
- 补 0:用
COALESCE(AVG(x) OVER (...), 0) - 保持首行无值(因无前置数据):不干预,这是合理行为
- 若原始数据列
sales本身含 NULL,AVG()会跳过它;若想把 NULL 当 0 算,得先COALESCE(sales, 0)再套窗口
性能关键:避免在大表上用 UNBOUNDED 或高开销排序
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 是累计平均,不是移动平均;真要算 30 日移动平均却写成 UNBOUNDED,会导致每行扫描全量历史,执行变慢十倍以上。
- 移动平均必须用固定长度窗口,如
ROWS BETWEEN 29 PRECEDING AND CURRENT ROW(30 日) - 确保
ORDER BY字段有索引,尤其是时间字段(如CREATE INDEX idx_on_date ON sales(date);) - PostgreSQL 和 ClickHouse 对
ROWS窗口优化较好;MySQL 8.0+ 支持,但旧版本不支持窗口函数,会直接报错ERROR 1064
示例:计算按日期排序的 5 日销售额移动平均
SELECT
date,
sales,
ROUND(AVG(sales) OVER (
ORDER BY date
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
), 2) AS moving_avg_5d
FROM sales
ORDER BY date;
注意这里没写 PARTITION BY,因为是全量时间序列;如果要按每个产品单独算移动平均,就得加上 PARTITION BY product_id,否则不同产品的数据会混在一起滑动。
实际部署时最容易被忽略的是:时间字段存在重复值(比如同天多笔订单),此时仅靠 ORDER BY date 无法保证确定性排序,必须加入唯一列(如 ORDER BY date, id),否则同一日期内行序不确定,移动平均结果每次执行都可能微调。











