要实现真正的移动平均(如7日均值),必须显式指定窗口框架,如ROWS BETWEEN 6 PRECEDING AND CURRENT ROW;默认UNBOUNDED PRECEDING TO CURRENT ROW是累积平均而非滑动平均。

SQL视图里直接用 AVG() 加 OVER() 就能算移动平均,但必须指定窗口框架
很多用户以为写个 AVG(column) OVER (ORDER BY date) 就完事了,结果发现值全一样——这是因为没定义窗口范围,默认是 UNBOUNDED PRECEDING TO CURRENT ROW,也就是从第一行累加到当前行,不是“最近 N 天”的滑动效果。
要实现真正的移动平均(比如 7 日均值),必须显式写出 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 或 RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW(取决于数据库是否支持 RANGE + 时间间隔)。
- PostgreSQL / SQL Server / Oracle 支持
ROWS和带INTERVAL的RANGE(需列是日期类型) - MySQL 8.0+ 支持
ROWS,但不支持RANGE中的INTERVAL,得先用ROW_NUMBER()或自连接模拟 - SQLite 3.25+ 支持
ROWS,但不支持RANGE+ 时间,只能按行数滑动
示例(PostgreSQL):
CREATE VIEW sales_7day_avg AS
SELECT
sale_date,
amount,
AVG(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS avg_7day
FROM sales;ORDER BY 在 OVER() 里不是可选项,漏写会报错或结果错乱
窗口函数要求排序确定性,否则数据库无法知道“前几行”是谁。有些引擎(如旧版 MySQL)允许不写 ORDER BY,但结果不可预测;PostgreSQL 直接报错 window definition requires an ORDER BY clause。
常见错误场景:
- 按
id排序但id不连续(比如有删改),导致“前 6 行”不是时间上最近的 6 条 - 用
created_at排序,但该字段精度为秒,多条记录同秒时顺序不确定 → 必须加二级排序,如ORDER BY created_at, id - 在视图中用
ORDER BY只影响窗口计算,不影响最终查询结果顺序;如需固定输出顺序,外部查询还得再写ORDER BY
视图里用窗口函数要注意物化与性能边界
视图本身不存储数据,每次查询都会重跑窗口计算。如果底表很大(比如千万级销售记录),AVG() OVER (... ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) 看似轻量,但数据库仍需对每行做局部扫描,I/O 和 CPU 开销随数据量线性增长。
- 避免在视图里嵌套多层窗口函数(比如先算移动平均,再对它算标准差),会显著拖慢响应
- 如果只查最近 30 天,别让视图扫描全表:要么在视图定义里加
WHERE sale_date >= CURRENT_DATE - INTERVAL '30 days'(注意这会让视图失去通用性) - 某些数据库(如 PostgreSQL)支持物化视图,可定期刷新结果,但需手动维护刷新逻辑和权限
NULL 值会让移动平均跳变,得提前决定怎么处理
AVG() 默认忽略 NULL,但窗口框架内若某天 amount 是 NULL,它就不参与分母计数——比如 7 行里有 2 个 NULL,实际是除以 5,不是除以 7。业务上可能需要“强制补零”或“向前填充”。
- 补零:用
COALESCE(amount, 0)再套窗口函数 - 向前填充(last observation carried forward):需配合
FIRST_VALUE()或自定义逻辑,纯窗口函数搞不定 - 检查空值比例:先运行
SELECT COUNT(*) FILTER (WHERE amount IS NULL) * 100.0 / COUNT(*) FROM sales,超 5% 就得和业务确认规则
真正麻烦的是时间不连续——比如周末无销售,ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 会跨过空白日取更早的数据,造成“平均周期失真”。这时必须用 RANGE + INTERVAL(如果支持),或先生成日期维度表做左连接。

















