AVG() OVER(PARTITION BY col1 ORDER BY col2 ROWS BETWEEN n PRECEDING AND CURRENT ROW)是唯一标准写法,必须同时含PARTITION BY、ORDER BY和ROWS BETWEEN,缺一不可。

直接用 AVG() 或 SUM() 配合 OVER() 的 ROWS BETWEEN 就能实现,但必须带 PARTITION BY + ORDER BY,漏掉任一条件都会跨组混算或报错。
必须写 PARTITION BY 和 ORDER BY
组内平滑的前提是“组内独立计算”,不加 PARTITION BY 就变成全表滚动;不加 ORDER BY 则 ROWS BETWEEN 无意义——MySQL 会报 Window 'w' requires an ordering clause,PostgreSQL 和 SQL Server 同样拒绝执行。
-
PARTITION BY user_id确保 Alice 的数据不会和 Bob 的混在一起算平均 -
ORDER BY dt决定哪几行属于“当前窗口”,时间字段必须业务有序(如不能用插入顺序 ID 替代真实时间) - 若
dt有重复值,建议补一个唯一列:ORDER BY dt, id,避免排序不稳定
ROWS BETWEEN 的边界行为要心里有数
数据库对开头几行的处理默认是“自动截断”,不是报错也不是补 NULL:第 1 行窗口只有自身,第 2 行含前 1 行 + 当前行,第 3 行起才满指定行数。这符合多数业务场景,但如果你需要“严格 3 行才计算”,就得套 CASE WHEN COUNT(*) OVER (...) = 3 THEN ... END。
-
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW→ 最多 3 行,含当前行 -
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING→ 对称窗口(PostgreSQL/SQL Server 支持,MySQL 8.0+ 也支持) - 别用
RANGE替代ROWS:时间字段用RANGE BETWEEN INTERVAL '2 days' PRECEDING看似合理,但 MySQL 不支持,PostgreSQL 在非等距数据下会引入意外聚合
NULL 值和分母陷阱不能靠猜
AVG(value) 会自动忽略 NULL,但如果你用 SUM(value) / (N + 1) 手动算移动平均,SUM() 遇到 NULL 返回 NULL,整个结果就崩了。分母也不能用 COUNT(*) OVER (),它会计入 NULL 行,而 SUM() 不计,导致分子分母不匹配。
- 业务上允许“无数据=0”:用
COALESCE(value, 0)再进窗口函数 - 业务上必须排除缺失值:用
SUM(value) / NULLIF(COUNT(value) OVER (...), 0) - 别在
OVER()里写函数,例如ORDER BY DATE(dt)会让索引失效
性能卡点往往藏在索引和数据密度里
窗口函数本身不慢,慢的是没索引的 ORDER BY 字段,或者窗口跨度远大于数据实际密度。比如按天算 30 天移动平均,但数据里每月只有一条记录,那窗口实际只覆盖 1 行,却仍要扫描 30 行范围——数据库无法跳过空档。
- 确保
ORDER BY字段(如dt)有 B-tree 索引 - 如果原始数据稀疏(如月度汇总),先用
generate_series()(PostgreSQL)或日期维表补全,再跑窗口 - 千万级以上表慎用大跨度窗口(如
99 PRECEDING),考虑预计算或降采样
真正容易被忽略的不是语法,而是“排序依据是否真实反映业务时序”和“窗口大小是否匹配数据粒度”。比如用自增 ID 排序算用户行为移动平均,ID 有序但时间未必有序;又比如在每小时只有一条记录的表上强行跑 7 天滑动窗口,窗口内大部分是空的,均值毫无意义。










