rows按物理行号计数,range按排序列值范围匹配;前者严格取固定行数,后者将同值行全纳入,业务语义错配会导致结果偏差且不报错。

ROWS 和 RANGE 不是“选哪个更快”的问题,而是“业务语义是否匹配”的问题——用错一个,结果可能完全对不上业务口径,且不报错、难复现。
ROWS 是按行号数数,RANGE 是按值划区间
ROWS 看的是物理顺序:当前行、往前 2 行、往后 1 行……它不管这些行的 event_time 是不是同一天、price 是不是一样,只认位置。
RANGE 看的是排序字段的值:所有 event_time 落在“当前行时间减去 7 天”到“当前行时间”之间的行都算进来——哪怕中间缺了 3 天,某天有 200 笔订单,它也全收。
常见错误现象:
-
SUM(sales) OVER (ORDER BY event_time RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW)在节假日当天有 500 笔单,结果突然跳升;换成ROWS BETWEEN 6 PRECEDING AND CURRENT ROW就变成“最近 7 笔”,跟时间完全脱钩 - 用
LAG(amount) OVER (ORDER BY price RANGE)直接报错或返回NULL——LAG/LEAD只支持ROWS,因为“上一行”必须是确定位置,不能是“上一个价格区间”
重复值会让 RANGE “吞行”,ROWS 不会
当 ORDER BY 字段存在大量重复(比如 DATE(created_at)、状态码 status、分档字段 score_level),RANGE 会把所有同值行视为同一逻辑点。
例如:
-
ORDER BY status,前面有 80 行status = 1,当前行也是status = 1,那么RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW实际包含全部 81 行 - 同样场景下,
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW严格按行号累计,第 81 行就只加第 81 行的值 - 这直接导致累计求和在爆发日突增、移动平均在重复时间点异常放大——不是计算错,是语义被悄悄替换了
默认隐式补 RANGE,但你根本不知道
几乎所有数据库(PostgreSQL / MySQL 8.0+ / SQL Server)在你只写 SUM(x) OVER (ORDER BY ts) 时,都会自动补成 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。
这个行为不会报错,也不在执行计划里高亮提示,但后果很实在:
- 如果
ts是DATETIME且毫秒级精度低(比如聚合到秒),大量重复值触发 RANGE 吞行 - 性能可能断崖下跌:RANGE 最坏 O(n²),ROWS 是 O(1) 游标偏移;真实数据跑
EXPLAIN ANALYZE,常差 3–10 倍 - MySQL 8.0+ 对
RANGE支持更保守:RANGE BETWEEN 7 PRECEDING AND CURRENT ROW会语法报错,只接受UNBOUNDED或CURRENT ROW配合INTERVAL(且仅限数值/日期类型)
什么时候必须用 RANGE?别硬套 ROWS
不是性能优先就无脑换 ROWS。以下场景 RANGE 不可替代,强行改用 ROWS 必然出错:
- “过去 7 天所有订单金额总和” → 必须
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW;用ROWS BETWEEN 7 PRECEDING就变成“最近 7 笔”,跟时间无关 - “价格 ±50 元内的竞品均价” →
RANGE BETWEEN 50 PRECEDING AND 50 FOLLOWING,这里 50 是数值差,不是行数 - “所有
score >= 当前用户 score的人数” →RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING,靠值比较而非位置 - 注意:
ORDER BY字段必须可比较且非字符类型(如VARCHAR不支持RANGE边界计算);含NULL时需显式写NULLS LAST,否则边界漂移
真正难的不是写法,是判断业务到底依赖“位置”还是“值域”——比如“最近 N 笔订单”是位置问题,用 ROWS;“最近 N 天内所有订单”是值域问题,用 RANGE 更安全。上线前务必用真实数据量跑 EXPLAIN ANALYZE,对比两者 Actual Total Time 和结果集差异。










