SQL窗口函数允许我们在不改变结果集行数的前提下,对每一行执行基于相邻行的聚合或排序计算,而ORDER BY子句的加入会默认改变窗口的范围定义,很多开发者因此遇到计算结果不符合预期的情况,要解决这个问题就需要理清ROWS和RANGE两种范围模式的区别。

窗口函数的基本框架概念
窗口函数的完整语法中,OVER子句除了可以指定PARTITION BY分组和ORDER BY排序外,还可以定义窗口框架,也就是确定每一行计算时参考哪些相邻行。如果不手动指定框架,默认的行为是:没有ORDER BY时,框架是整个分区;有ORDER BY时,默认框架是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,这也是很多范围问题的来源。
ROWS与RANGE的核心区别
两种模式的核心差异在于确定参考行的逻辑:
- ROWS:按照行的物理位置来确定范围,直接指定当前行前后多少行参与计算,和行的值无关。
- RANGE:按照ORDER BY指定的排序字段的值来确定范围,所有和当前行排序字段值相同的行都会被纳入范围,和行的物理位置无关。
示例演示差异
我们先创建一张测试表,插入如下数据:
-- 创建测试表
CREATE TABLE sales (
sale_id INT,
sale_month INT,
sale_amount INT
);
-- 插入测试数据
INSERT INTO sales VALUES
(1, 1, 100),
(2, 1, 100),
(3, 2, 200),
(4, 3, 150);
使用ROWS模式的计算结果
我们按sale_month排序,计算每行及之前所有行的累计销售额,使用ROWS模式:
SELECT
sale_id,
sale_month,
sale_amount,
-- 物理行范围:从第一行到当前行
SUM(sale_amount) OVER (
ORDER BY sale_month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS rows_cumulative
FROM sales;
执行结果如下:
| sale_id | sale_month | sale_amount | rows_cumulative |
|---|---|---|---|
| 1 | 1 | 100 | 100 |
| 2 | 1 | 100 | 200 |
| 3 | 2 | 200 | 400 |
| 4 | 3 | 150 | 550 |
可以看到,sale_month为1的两行是分别计算累计值,因为ROWS按物理行计数,第二行的累计值是前两行的和。
使用RANGE模式的计算结果
同样的排序逻辑,改用RANGE模式:
SELECT
sale_id,
sale_month,
sale_amount,
-- 值范围:所有sale_month小于等于当前行sale_month的行
SUM(sale_amount) OVER (
ORDER BY sale_month
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS range_cumulative
FROM sales;
执行结果如下:
| sale_id | sale_month | sale_amount | range_cumulative |
|---|---|---|---|
| 1 | 1 | 100 | 200 |
| 2 | 1 | 100 | 200 |
| 3 | 2 | 200 | 400 |
| 4 | 3 | 150 | 550 | >
此时sale_month为1的两行累计值都是200,因为RANGE模式下所有sale_month等于1的行都会被纳入同一个范围,两行的sale_amount之和就是200。
如何解决ORDER BY带来的范围问题
要解决这类问题,只需要根据实际需求手动指定框架模式即可:
- 如果需要按物理行计算,比如计算每个员工和前一名员工的工资差,就显式指定ROWS模式。
- 如果需要按排序字段的值分组计算,比如计算同月份的所有销售额总和作为累计值,就使用默认的RANGE模式,或者显式指定。
- 如果不需要排序字段影响范围,只是想排序后计算整个分区的聚合值,就不要加ORDER BY,或者手动指定框架为
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。
例如,如果我们想按sale_month排序,但累计值计算所有行的销售额,而不是受ORDER BY默认范围影响,可以这样写:
SELECT
sale_id,
sale_month,
sale_amount,
SUM(sale_amount) OVER (
ORDER BY sale_month
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS total_amount
FROM sales;
这样不管排序如何,每一行的total_amount都是所有行的销售额总和,避免了默认RANGE范围带来的计算偏差。