在做数据分析时,我们经常遇到这类需求:计算每个用户最近3笔订单的平均金额、统计每7天的滑动销量、或者求每个商品当前记录与上一条记录的差值。这些需求的共同特点是聚合范围不是整个分组,而是一个以当前行为中心的固定行数窗口。普通的GROUP BY无法胜任这类计算,因为GROUP BY会把多行压缩成一行,而窗口函数可以在保留每一行的同时进行聚合,其中ROWS BETWEEN子句正是用来精确控制这个窗口边界的关键语法。

一、窗口函数的基本结构与ROWS BETWEEN的位置
要理解ROWS BETWEEN,先要清楚窗口函数的完整语法结构。一个典型的窗口函数调用包含三部分:函数名(如SUM、AVG、COUNT、MAX)、OVER子句,以及OVER内部的三个可选部分——PARTITION BY分区、ORDER BY排序、窗口框架FRAME。ROWS BETWEEN就属于最后一部分,它定义了相对于当前行,聚合计算要覆盖哪些行。
完整语法如下:
SELECT
SUM(amount) OVER (
PARTITION BY product_id -- 分区:按商品划分
ORDER BY sale_date -- 排序:决定行的先后顺序
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 框架:当前行及前面2行
) AS moving_sum
FROM sales;
这段代码中,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示窗口从当前行往上数2行开始,一直到当前行结束,共3行参与求和。ROWS BETWEEN的起点和终点各有多种写法,常用的组合包括:
- UNBOUNDED PRECEDING:分区内第一行,也就是从最开头算起
- N PRECEDING:当前行之前的第N行
- CURRENT ROW:当前行本身
- N FOLLOWING:当前行之后的第N行
- UNBOUNDED FOLLOWING:分区内最后一行
需要注意的一个细节是,当ORDER BY子句存在而未显式指定框架时,很多数据库(如PostgreSQL、SQL Server)的默认框架是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,而不是很多人以为的ROWS形式。这两者在排序值存在并列时结果会不一样,这是实际使用中最容易踩的坑之一,后面会详细对比。
二、典型应用场景:滑动平均与累计求和
以一张销售明细表sales为例,表结构为(id, product_id, sale_date, amount),我们来看看ROWS BETWEEN能解决哪些实际问题。
第一个场景是计算三日均销量。电商运营中经常需要观察销量的短期趋势,消除单日波动带来的干扰,滑动平均是最常用的指标:
SELECT
product_id,
sale_date,
amount,
ROUND(AVG(amount) OVER (
PARTITION BY product_id
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2) AS avg_3days
FROM sales
ORDER BY product_id, sale_date;
这里窗口覆盖的是当前行加前面两行,也就是最近三个交易日的数据。如果开头几行不足3行,比如分区的第一行,窗口就只有1行,平均值的分母也相应变小,这是符合预期的自然行为。如果业务上要求必须满3行才计算,可以额外配合COUNT判断行数:
SELECT
product_id,
sale_date,
CASE
WHEN COUNT(*) OVER w >= 3
THEN ROUND(AVG(amount) OVER w, 2)
ELSE NULL
END AS avg_3days
FROM sales
WINDOW w AS (
PARTITION BY product_id
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
);
第二个场景是累计求和。把窗口起点设为UNBOUNDED PRECEDING,终点设为CURRENT ROW,就能得到从分区开头到当前行的累计值,常用于监控当日累计交易额、进度跟踪等:
SELECT
product_id,
sale_date,
amount,
SUM(amount) OVER (
PARTITION BY product_id
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM sales;
第三个场景是居中窗口。窗口不必以当前行为终点,例如ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING覆盖前一行、当前行、后一行共3行,适合计算某个值与相邻记录的整体对比,比如检测异常点时看当前值是否明显偏离前后均值。
三、ROWS与RANGE的区别及使用注意事项
ROWS和RANGE是两种窗口模式,写法上只差一个关键字,行为却截然不同。ROWS按物理行数计算窗口边界,RANGE按ORDER BY列的逻辑值计算边界。当排序列存在相同值时,RANGE会把所有与当前行排序值相同的行都纳入窗口,即使它们物理上排在当前行后面。
举个例子,假设表中有三行数据的sale_date都是2024-01-01,金额分别为10、20、30。使用RANGE ... UNBOUNDED PRECEDING AND CURRENT ROW时,这三行的累计结果都是60,因为RANGE认为排序值等于当前值的行都属于窗口范围;而使用ROWS时,三行的累计结果分别是10、30、60。绝大多数滑动窗口场景下,我们想要的是ROWS的物理行行为,所以建议在需要精确控制行数时显式写ROWS。
除了ROWS与RANGE的选择,还有几个实践要点值得注意。第一,ORDER BY不可省略。如果不写ORDER BY,整个分区会被当作一个窗口,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW这样的子句会直接报语法错误,因为窗口框架依赖行的确定顺序。第二,排序的稳定性会影响结果的确定性。当排序键存在并列时,不同数据库甚至同一数据库的不同执行可能得到不同的行顺序,建议在排序列末尾追加一个唯一列(比如自增ID)作为兜底排序,保证结果可复现。第三是性能问题,窗口函数需要对分区内的数据进行排序,数据量大时排序开销明显,合理利用排序列上的索引、减少不必要的分区列,可以显著降低执行成本。
最后提一下兼容性。ROWS BETWEEN在主流数据库中都得到了支持,包括MySQL 8.0及以上、PostgreSQL、Oracle、SQL Server、Hive以及大数据引擎Spark SQL等。如果使用的是MySQL 5.7这样的老版本,窗口函数整体不可用,只能通过自连接或用户变量模拟,写法复杂且性能较差,这种情况下建议升级数据库版本或把计算逻辑上移到应用层处理。
SQL窗口函数ROWS BETWEEN聚合计算修改时间:2026-09-16 05:38:33