SQL标准规定:只要OVER()中出现ORDER BY且未显式声明帧子句,就默认启用RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,旨在支持累积计算,但会导致LAST_VALUE()等函数行为反直觉。

窗口函数里不写帧子句时,ORDER BY 为什么自动配 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
因为 SQL 标准规定:只要 OVER() 中出现 ORDER BY,且未显式声明 ROWS 或 RANGE 帧子句,就默认启用 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。这不是数据库“偷懒”,而是标准行为,PostgreSQL、SQL Server、MySQL 8.0+、BigQuery 全部一致。
这个默认帧的设计初衷是支持「累积计算」——比如 SUM(sales) OVER (ORDER BY date) 想表达的就是“截至今天为止的总销售额”,而不是全量总和。但副作用也很明显:它让 LAST_VALUE()、LAG()、LEAD() 等函数的行为变得反直觉。
-
LAST_VALUE(x) OVER (ORDER BY ts)总返回当前行的x,不是分区末尾值——因为“最后”只在CURRENT ROW及之前生效 -
LAG(x, 1) OVER (ORDER BY ts)在第一行返回NULL,因为前面没行;但如果你本意是“取分区里上一条非空记录”,这个默认帧就不够用 - 当
ORDER BY字段存在重复值(如多个相同ts),RANGE帧会把它们全视为“同一位置”,导致ROW_NUMBER()跳号、LAST_VALUE()随机取其中一个
ROWS 和 RANGE 的实际差异在哪?为什么推荐优先用 ROWS
RANGE 按排序值的“语义区间”划分窗口,ROWS 按物理行序(即排序后的位置编号)划分。有重复值时,RANGE 会把所有同值行打包进同一帧,而 ROWS 严格按第 1 行、第 2 行……计数。
例如对时间戳字段 ts 排序后有三行并列同一秒:
– RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:这三行的窗口帧完全一样,LAST_VALUE() 可能取到其中任意一个
– ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:每行窗口只包含自己及前面所有行,第三行的帧明确包含全部三行
- 想取分组内真实最后一行?必须写
LAST_VALUE(x) OVER (PARTITION BY id ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 想取上一行固定偏移?
LAG(x, 1) OVER (ORDER BY ts ROWS)比RANGE更稳定,尤其在去重或分页场景 - SQLite 和旧版 MySQL 不支持
UNBOUNDED FOLLOWING,得用子查询先算出最大序号再关联,不能硬套标准语法
ORDER BY 在窗口函数里到底控制什么,又不控制什么
它只控制「窗口内计算所依赖的行序」,不控制最终结果集的展示顺序。外层 ORDER BY 才管输出顺序,两者完全解耦。
- 没写
ORDER BY:像AVG(x) OVER (PARTITION BY y)仍可运行,但结果是静态均值;而ROW_NUMBER() OVER (PARTITION BY y)会报错——排名类函数强制要求排序依据 - 写了
ORDER BY但漏了PARTITION BY:整个结果集被当成单一分区处理,ROW_NUMBER()从 1 排到 N,容易误以为是按组编号 -
ORDER BY字段含NULL:PostgreSQL 默认NULLS FIRST,MySQL 8.0+ 默认NULLS LAST,会导致同一语句在不同库中LAG()取值位置不一致
哪些情况必须显式写帧子句,不能依赖默认
只要你的业务逻辑依赖“整个分区范围”或“确定性行偏移”,就不能靠默认帧。典型场景包括:取分组末值、滚动 N 日统计、跨行比较固定距离(如环比)、避免重复值干扰。
- 要取每个用户最新一条订单的金额:
FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY order_time DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 计算过去 7 天销售额滚动和:
SUM(sales) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)—— 这里ROWS是必须的,RANGE无法表达“天数偏移” - 当排序字段可能为
NULL,又必须保证首行/末行稳定:显式加NULLS LAST或NULLS FIRST,再配ROWS帧
最常被忽略的一点:窗口函数基于的是未被 LIMIT 截断的完整中间结果集。你在外层加 LIMIT 10,不会影响 ROW_NUMBER() OVER (ORDER BY x) 的编号逻辑——它仍然对全部数据编号,只是最后只显示前 10 行。

















