SQL中无原生MOVING_AVERAGE()函数,必须用AVG()配合OVER()窗口函数及ROWS BETWEEN子句实现,如ROWS BETWEEN 2 PRECEDING AND CURRENT ROW计算3期移动平均,且必须显式ORDER BY确保行序稳定。

SQL里没有MOVING_AVERAGE()函数,得靠窗口函数
绝大多数SQL方言(PostgreSQL、SQL Server、BigQuery、Snowflake、MySQL 8.0+)不提供原生移动平均函数,必须用AVG()配合OVER()窗口子句实现。核心是定义正确的ROWS BETWEEN范围——它决定“移动”的窗口大小和方向。
常见错误是写成ORDER BY timestamp ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,这算的是累积平均,不是固定宽度的移动平均。真正需要的是类似ROWS BETWEEN 2 PRECEDING AND CURRENT ROW(含当前行共3期)或ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING(前后各1期,共3期对称窗口)。
实操建议:
- 先确认时间字段是否严格有序且无重复;若有重复,需加二级排序(如
ORDER BY ts, id),否则ROWS行为不可控 - 窗口宽度选奇数更直观(如3/5/7),便于理解“中心对齐”;偶数宽度会导致偏移,需明确业务是否接受
- 注意
CURRENT ROW是否包含在内——它默认包含,但有人误以为只算“过去”
PostgreSQL和MySQL 8.0+语法基本一致,但SQLite不支持
PostgreSQL和MySQL 8.0+都支持标准窗口函数语法,可直接复用。例如计算7日移动平均销售额:
SELECT
sale_date,
amount,
AVG(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma7
FROM sales;
SQLite直到3.25.0才支持窗口函数,旧版会报错near "OVER": syntax error。若必须用SQLite,只能用自连接或子查询模拟,性能差且易出错。
实操建议:
- 运行前先查版本:
SELECT version();(PostgreSQL)、SELECT VERSION();(MySQL) - BigQuery中
ORDER BY字段必须是确定性排序(不能是RAND()),否则报Window function ORDER BY expression must be deterministic - 如果数据按天聚合但存在空缺日期(如周末无销售),移动平均会跳过空行,导致窗口实际跨度变大——此时应先用
GENERATE_DATE_ARRAY(BigQuery)或递归CTE补全日期
处理NULL值和边界行时,结果常被悄悄截断
窗口函数在开头几行无法凑满指定行数时,默认返回部分窗口的平均值(如第1行只有自己,就返回amount本身),而非NULL。这容易掩盖数据稀疏问题。更麻烦的是,若amount本身为NULL,AVG()会自动忽略它——但你可能希望把NULL视作0或触发告警。
实操建议:
- 显式控制NULL行为:用
COALESCE(amount, 0)填充,或用CASE WHEN amount IS NULL THEN NULL ELSE AVG(...) END保留缺失语义 - 强制首尾N行返回
NULL:加条件判断COUNT(*) OVER (...) < 7 THEN NULL ELSE ...,避免用不足7天的数据误导分析 - 检查结果列是否有意外
NULL:移动平均列出现NULL通常意味着窗口内所有值都是NULL,而非计算失败
大数据量下,ROWS BETWEEN比RANGE BETWEEN更安全
用RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW看似更符合业务逻辑(按时间跨度而非行数),但隐患很大:一旦时间字段有重复值,RANGE会把所有同时间点的行全拉进窗口,导致窗口大小失控。而ROWS严格按物理顺序取固定行数,稳定可控。
实操建议:
- 永远优先用
ROWS,除非业务明确要求“过去7个自然日”,且已确保时间字段无重复、无乱序 - 若真要用
RANGE,务必加唯一约束或去重预处理,否则某天突发大量订单会让当天所有记录挤进同一个窗口 - 在WHERE里提前过滤掉无效时间(如
sale_date > '2020-01-01'),避免窗口扫描全表历史数据拖慢查询

















