用 rows between n preceding and current row 计算向前含自身共 n+1 行的移动平均;需确保 order by 唯一,否则结果不稳定;开头帧不足时可用 coalesce(avg(...) over (...), sales) 补原始值。

SQL窗口函数中ROWS BETWEEN怎么定义移动平均的计算范围
直接说结论:用 ROWS BETWEEN n PRECEDING AND CURRENT ROW 可以算「向前含自身共 n+1 行」的移动平均;若要包含前后行(如中心滑动窗口),得用 ROWS BETWEEN n PRECEDING AND n FOLLOWING。别硬套 Excel 里的“最近3期”说法——SQL 窗口边界是**基于行偏移量**,不是基于时间或值。
常见错误现象:AVG(x) OVER (ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) 在 ts 有重复值时结果不稳定,因为 ORDER BY 相同值的行顺序不保证,导致 ROWS 框选的行可能每次不同。
- 必须显式指定
ORDER BY,否则ROWS BETWEEN无意义(默认无序,CURRENT ROW不确定) - 如果业务要求“按时间对齐”,但时间字段有重复,建议加二级排序,例如
ORDER BY ts, id -
PRECEDING和FOLLOWING的数字是**行数**,不是天数/月份;想按时间跨度算,得先生成时间序列或用RANGE BETWEEN(但注意:PostgreSQL 支持RANGE配合时间类型,MySQL 8.0+ 仅支持数值型RANGE)
MySQL 8.0 和 PostgreSQL 中移动平均的写法差异
核心差异不在语法,而在对空值和边界行的处理逻辑是否一致。两者都支持 ROWS BETWEEN,但默认行为略有不同。
比如计算每行及前两行的均价:
SELECT
day,
sales,
AVG(sales) OVER (
ORDER BY day
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3
FROM daily_sales;
- MySQL 8.0:自动跳过
NULL值参与计算(AVG本身语义),但窗口帧仍包含这些NULL行位置 - PostgreSQL:同样跳过
NULL,但如果整帧都是NULL,返回NULL;而 MySQL 在首行可能因帧不足仍尝试计算(实际只有一行非空,就直接返回那行值) - 性能影响:窗口越大,帧内排序和累计计算开销越明显;在千万级表上,避免在
WHERE之前用大窗口,先过滤再开窗
如何避免移动平均结果出现“开头几行全为NULL”
这不是 bug,是 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 在第一、二行时帧不完整导致的正常行为。但业务常需要“能算尽算”,比如第一行就显示自身值。
- 不用强行补数据,改用
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW是累积平均,不是移动平均 - 正确做法:用
CASE判断当前帧内实际非空行数,再控制分母——但多数引擎不支持在窗口函数里动态算帧长 - 更务实的解法:用
COALESCE(AVG(...) OVER (...), sales),把开头几行的NULL替换为原始值(前提是接受“首行=自身值,第二行=自身与前一行均值”这种逻辑) - 如果必须严格 N 行滚动且首尾补值,得用自连接或 LATERAL(PostgreSQL)/JOIN + 子查询,但性能差,仅小数据量可用
当 ORDER BY 字段不是主键时,移动平均为什么每次执行结果不一样
根本原因是:窗口帧依赖 ORDER BY 定义的逻辑顺序,而该顺序在存在并列值时未完全确定。数据库只要满足“相同 ORDER BY 值的行相对顺序任意”,就算符合 SQL 标准。
- 典型场景:按
date排序,但一天有多条记录,又没指定第二排序字段 - 后果:两次查询中,同一日期内的多行被纳入窗口帧的顺序不同 →
2 PRECEDING指向的行不同 → 移动平均值波动 - 修复方式:强制唯一排序,例如
ORDER BY date, id或ORDER BY date, ROW_NUMBER() OVER (PARTITION BY date ORDER BY created_at) - 别依赖索引顺序来“碰运气”,SQL 层面不保证
id,也别让 ROWS BETWEEN 在模糊排序下运行。










