SQL中计算移动平均值是处理时间序列数据平滑的常见需求,通过滑动窗口函数可以高效实现这一功能,不需要复杂的自连接操作,就能快速得到指定窗口范围内的平均值结果。

什么是移动平均值
移动平均值也叫滑动平均值,是指根据固定间隔的连续数据计算平均值,每新增一个数据点,就去掉窗口最前端的数据点,重新计算平均值。这种计算方式可以消除数据中的随机波动,让趋势更清晰,常用于销售趋势分析、传感器数据平滑、股价走势判断等场景。
滑动窗口函数基础语法
SQL中计算移动平均值主要使用AVG()聚合函数配合OVER()子句实现,核心是通过ROWS或者RANGE指定滑动窗口的范围。ROWS是基于行的物理偏移,RANGE是基于值的逻辑偏移,计算移动平均值时通常使用ROWS更直观。
基础语法结构如下:
AVG(目标列) OVER (
PARTITION BY 分组列
ORDER BY 排序列
ROWS BETWEEN 前偏移行 AND 后偏移行
) AS 移动平均值别名
不同场景下的实现示例
场景一:计算最近3天的移动平均销售额
假设我们有一张销售记录表sales_record,包含sale_date(销售日期)、amount(销售金额)两个字段,现在需要计算每条记录对应的最近3天(包含当天)的移动平均销售额。
实现代码如下:
SELECT
sale_date,
amount,
-- 计算最近3天(当前行+前2行)的移动平均销售额
AVG(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3d
FROM sales_record
ORDER BY sale_date;
上述代码中,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示窗口范围是当前行的前2行到当前行,总共3行数据,对这3行的amount求平均就是最近3天的移动平均值。如果某条记录前面不足2行,窗口会自动调整为实际存在的行数,不会报错。
场景二:按分组计算移动平均值
如果需要按不同商品分组计算各自的移动平均值,只需要添加PARTITION BY子句即可。比如sales_record表新增product_id(商品ID)字段,计算每个商品最近3天的移动平均销售额:
SELECT
product_id,
sale_date,
amount,
AVG(amount) OVER (
PARTITION BY product_id
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS product_moving_avg_3d
FROM sales_record
ORDER BY product_id, sale_date;
PARTITION BY product_id会让窗口函数单独在每个商品的分组内计算,不同商品的移动平均值互不影响。
场景三:计算中心移动平均值
如果需要计算以当前行为中心的移动平均值,比如当前行前后各1行,总共3行数据的平均值,调整窗口范围即可:
SELECT
sale_date,
amount,
-- 计算当前行前后各1行的中心移动平均值
AVG(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS center_moving_avg
FROM sales_record
ORDER BY sale_date;
注意事项
- 排序字段必须唯一或者逻辑上符合时间顺序,否则窗口内的数据范围可能不符合预期,导致计算结果错误。
- 如果数据中存在
NULL值,AVG()函数会自动忽略NULL值,计算时需要注意NULL对结果的影响,必要时可以先处理NULL值再计算。 - 不同数据库对滑动窗口函数的支持略有差异,MySQL 8.0+、PostgreSQL、SQL Server、Oracle等主流数据库都支持该语法,低版本数据库可能需要用自连接的方式实现,效率会低很多。
自连接实现方式(兼容低版本数据库)
如果使用的数据库不支持滑动窗口函数,可以通过自连接实现移动平均值计算,以最近3天移动平均为例:
SELECT
t1.sale_date,
t1.amount,
AVG(t2.amount) AS moving_avg_3d
FROM sales_record t1
-- 自连接匹配最近3天的记录
LEFT JOIN sales_record t2
ON t2.sale_date >= DATE_SUB(t1.sale_date, INTERVAL 2 DAY)
AND t2.sale_date <= t1.sale_date
GROUP BY t1.sale_date, t1.amount
ORDER BY t1.sale_date;
这种方式需要两次扫描同一张表,数据量较大时性能会比滑动窗口函数差,优先推荐使用滑动窗口函数实现。