在数据分析场景中,我们经常需要针对某个分组(例如每个门店、每个用户)计算最近一段时间内的平均值,这种需求被称为移动平均或滚动平均。传统做法可能通过自连接或者相关子查询实现,不仅语句冗长,而且执行效率偏低。现代SQL提供的窗口函数允许我们在保留明细行的同时,对分组内的行集合做聚合计算,其中AVG函数结合OVER子句中的PARTITION BY与ROWS范围限定,是处理分组移动平均最直接的方式。

一、窗口函数与移动平均的基本原理
窗口函数与普通聚合函数最大的区别在于,它不会将多行压缩成一行,而是为每一行返回一个基于“窗口”计算结果的值。所谓窗口,就是当前行所处的一个行集合,我们可以通过PARTITION BY将其按字段分组,再通过ORDER BY定义组内顺序,最后用ROWS或RANGE指定窗口的物理或逻辑边界。
在计算移动平均时,ROWS BETWEEN N PRECEDING AND CURRENT ROW 是最常用的写法。它的含义是:对于组内的每一行,取其前面N行到当前行这个范围,对这个范围内的数值列求平均。由于窗口是在每个PARTITION内部独立滑动的,因此天然实现了分组数据的移动平均,而不需要手写复杂的JOIN条件。
1.1 核心语法结构
一个典型的AVG窗口函数语句如下。PARTITION BY dept_id表示按部门分组,ORDER BY sale_date确定组内按日期排序,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示取当前行及前两行(共三行)求平均。
SELECT
dept_id,
sale_date,
amount,
AVG(amount) OVER (
PARTITION BY dept_id
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3
FROM sales;
上述代码中,如果某部门前两天的数据不足(例如第一行只有自己,第二行只有自己及前一行),数据库会自动按实际存在的行数计算平均,不会报错。这种特性使得移动平均逻辑非常健壮,不需要额外处理边界情况。
二、ROWS与RANGE的区别及注意事项
在指定窗口范围时,SQL标准提供了ROWS和RANGE两种模式。ROWS是基于物理行偏移,例如ROWS BETWEEN 2 PRECEDING AND CURRENT ROW严格指向当前行之前的两条记录;而RANGE通常基于ORDER BY列的值区间,在存在相同排序值时会把并列行都纳入窗口,可能导致行数不可控。因此计算分组移动平均值时,优先使用ROWS能确保结果符合“最近N条记录”的预期。
另外需要注意,在部分旧版本数据库中,如果ORDER BY后的列存在NULL值,排序行为可能不稳定,从而影响滑动窗口的内容。建议在排序字段上增加主键或唯一列作为辅助排序,保证组内顺序唯一确定。
2.1 不同数据库的兼容写法
主流关系型数据库如 PostgreSQL、MySQL 8.0+、SQL Server 均支持上述ROWS语法。以下示例展示了在MySQL中建表并验证分组移动平均的完整过程:
CREATE TABLE sales (
dept_id INT,
sale_date DATE,
amount DECIMAL(10,2)
);
INSERT INTO sales VALUES
(1, '2023-01-01', 100.00),
(1, '2023-01-02', 150.00),
(1, '2023-01-03', 200.00),
(1, '2023-01-04', 50.00),
(2, '2023-01-01', 80.00),
(2, '2023-01-02', 120.00);
SELECT
dept_id,
sale_date,
amount,
AVG(amount) OVER (
PARTITION BY dept_id
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3
FROM sales;
执行后,dept_id为1的第四行(2023-01-04)的moving_avg_3将是150、200、50三天的平均,即133.33,而第一行仅为100.00自身。这清楚地体现了分组内滑动窗口的独立性与连续性。
三、实际业务中的扩展用法
除了最简单的“前N行到当前行”,我们还可以定义居中窗口,例如ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING计算前后各一行加自身的均值,适用于平滑时序曲线。若要结合时间跨度而非行数,可改用RANGE并配合INTERVAL,但需数据库支持相应语法。
在性能方面,窗口函数通常由数据库优化器一次性完成分组与排序,相比自连接少了重复扫描。对于大数据量,建议在PARTITION BY和ORDER BY涉及的列上建立复合索引,这样能显著减少排序阶段的临时文件读写,提高移动平均查询的响应速度。
3.1 与子查询方案的对比
若不使用窗口函数,我们可能写出如下相关子查询来计算同部门前三日均价:
SELECT
s1.dept_id,
s1.sale_date,
s1.amount,
(SELECT AVG(s2.amount)
FROM sales s2
WHERE s2.dept_id = s1.dept_id
AND s2.sale_date <= s1.sale_date
AND s2.sale_date >= DATE_SUB(s1.sale_date, INTERVAL 2 DAY)
) AS moving_avg_3
FROM sales s1;
该写法在每行都要执行一次子查询,且日期区间依赖具体时间类型,当数据缺失日期时容易漏算。而窗口函数ROWS方案以行数为基准,逻辑更直观,也更容易修改为任意N行窗口。因此在支持窗口函数的环境中,应优先采用AVG加ROWS的写法来处理分组移动平均需求。