SQL 窗口函数中的 ROWS 与 RANGE 区别

ROWS按物理行数定义窗口,RANGE按排序值逻辑范围定义窗口;ROWS严格计数行号,RANGE聚合同值组并按值域扩展,选型依“固定数量”或“值域区间”需求而定。

ROWS 和 RANGE 都是用来定义窗口函数中“当前行的邻居范围”,但它们的划分逻辑完全不同:ROWS 按物理行数切分,RANGE 按排序值的逻辑范围切分。

ROWS 是按行号计数的“固定长度滑动窗口”

ROWS 严格依据 ORDER BY 后的排序结果,从当前行出发,向上(PRECEDING)或向下(FOLLOWING)数指定数量的物理行。即使这些行的排序值相同,也各自独立计数。

  • 例如 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING 总是取当前行前后各 1 行,共 3 行,不管这三行的 ORDER BY 列值是否相等
  • 如果当前行是第 5 行,且前面有重复值,ROWS 仍只拉第 4、5、6 行,不会因为第 3 行和第 4 行值相同就多拉一行
  • 适用于需要“最近 N 笔交易”“前后 N 天记录”这类明确数量控制的场景

RANGE 是按排序值分组的“值域区间窗口”

RANGE 把 ORDER BY 列中值相等的所有行视为一个逻辑单元(即“同值组”),然后以当前行的排序值为中心,向左右扩展指定的值范围(需配合 UNBOUNDED 或数字常量使用),所有落在该值范围内的行都会被纳入窗口。

  • 例如 RANGE BETWEEN 10 PRECEDING AND CURRENT ROW 表示:取排序列值在 [当前行值 − 10, 当前行值] 区间内的所有行
  • 若 ORDER BY 列是金额,当前行为 100,则会包含所有金额在 90 到 100 之间的行(哪怕有 20 行都等于 95,全算进来)
  • 注意:RANGE 默认仅支持 ORDER BY 单一数值列(如 INT/FLOAT/DATE),不支持字符串或多个列

当 ORDER BY 值重复时,ROWS 和 RANGE 行为差异最明显

假设有以下按 score 排序的数据:

id | score
1  | 80
2  | 85
3  | 85
4  | 85
5  | 90

对每一行计算 COUNT(*) OVER (ORDER BY score ...)

  • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:每行的计数依次是 1,2,3,4,5(严格按行号累加)
  • RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:第 1 行得 1;第 2–4 行(score=85)

    都看到“从最小值到 85”的全部行,结果都是 4;第 5 行才变成 5

实际选型建议

  • 要“取最近 3 条记录”,选 ROWS;要“取价格相差不超过 5 的所有商品”,选 RANGE
  • 涉及时间字段(如 order_date)时,ROWS 不适合表达“过去 7 天”,而 RANGE 可写 RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW(PostgreSQL/Oracle 支持,MySQL 8.0+ 对 DATE 类型有限支持)
  • 默认框架(未显式写 ROWS/RANGE)在多数数据库中等价于 RANGE UNBOUNDED PRECEDING,这点容易踩坑,建议始终显式声明