SQL窗口函数中UNBOUNDED PRECEDING的使用方法详解
时间:2026-06-23 06:58
对于刚开始接触窗口函数的朋友来说,`UNBOUNDED PRECEDING` 这个概念确实容易让人有点困惑。它到底是个什么东西?真的能“从起点开始”吗? 先聊第一个问题。`UNBOUNDED PRECEDING` 不是指整个表物理上的第一行,而是指当前这个窗口框架(frame)在逻辑上能回溯到的最远
对于刚开始接触窗口函数的朋友来说,`UNBOUNDED PRECEDING` 这个概念确实容易让人有点困惑。它到底是个什么东西?真的能“从起点开始”吗?
先聊第一个问题。`UNBOUNDED PRECEDING` 不是指整个表物理上的第一行,而是指当前这个窗口框架(frame)在逻辑上能回溯到的最远边界。它的起点在哪里,完全由 `ORDER BY` 子句和分区(`PARTITION BY`)共同决定。没写 `PARTITION BY` 的话,那它回看的就是整个表;写了,就只看当前分区内部,从该分区排好序的第一行算起。
一个很常见的误解是,只要加了 `UNBOUNDED PRECEDING`,窗口就会自动累积所有的历史数据。结果跑出来一看,发现根本没累加起来,问题多半就出在漏写了 `ORDER BY`。按照 SQL 标准,如果没写 `ORDER BY`,窗口帧的默认定义是 `ROWS BETWEEN CURRENT ROW AND CURRENT ROW`,这时候 `UNBOUNDED PRECEDING` 其实是不起作用的。
所以这里有几个关键点需要记住:
* **必须搭配 `ORDER BY`**:这是激活帧定义的前提,缺了它就没法用。
* **分区的影响**:如果用了 `PARTITION BY order_date`,那 `UNBOUNDED PRECEDING` 的“起点”就是该 `order_date` 分区内排好序的第一行。
* **兼容性**:PostgreSQL、Snowflake、BigQuery、Spark SQL 都支持;MySQL 8.0+ 开始支持,5.7 不行。

怎么写一个真正从分区首行累加的 SUM 窗口?
再来看看一个典型的场景:按用户分组,按时间排序,计算每个订单的累计金额。写对的关键不只是写上 `UNBOUNDED PRECEDING`,而是整个帧子句要写完整:
SELECT
user_id,
order_time,
amount,
SUM(amount) OVER (
PARTITION BY user_id
ORDER BY order_time
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumsum
FROM orders;
这里需要留意三点:
1. **`ROWS BETWEEN ...` 要显式写出来**:有些数据库方言(比如 BigQuery)允许省略 `ROWS`,但语义可能会降级为 `RANGE`,导致重复值被合并计算,结果就不对了。
2. **`ORDER BY` 决定了“首行”是谁**:如果 `order_time` 有重复值,建议加个二级排序(比如 `ORDER BY order_time, order_id`),避免得到不稳定的结果。
3. **用 `ROWS` 而不是 `RANGE`**:除非你确实需要把相同 `order_time` 的多行看作一个整体,否则 `RANGE` 会把它们捆在一起计算,很可能不符合我们期望的“逐行累加”逻辑。
为什么有时候 UNBOUNDED PRECEDING 像没起作用?
这种情况在实际开发中并不少见:SQL 能跑通,没报错,但算出来的 `cumsum` 列每行都只等于当前行的 `amount`,看起来就像没累加。通常逃不出下面几种情况:
* **漏写 `ORDER BY`**:这是最常见的硬性错误,写了对窗口函数的理解还是不够深刻,少了它就退化为单行窗口。
* **误写成 `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`**:如果这时 `ORDER BY` 列恰好有重复值(比如多个订单在同一秒下单),`RANGE` 会把它们全部纳入当前帧,结果就和预期不一样了。
* **数据库方言差异**:不同数据库的默认行为不同。比如 SQL Server 默认用 `RANGE`,而 PostgreSQL 默认用 `ROWS`。如果不显式声明 `ROWS`,换一个数据库执行,结果可能就变了。
* **分区粒度太细**:如果 `PARTITION BY` 分得特别细(比如按“分钟”级的时间分区),导致每个分区只有一两行数据,那 `UNBOUNDED PRECEDING` 的起点自然就近了,效果也就不明显了。
UNBOUNDED PRECEDING 和 MAX() / FIRST_VALUE() 搭配要注意什么?
`UNBOUNDED PRECEDING` 不只是给 `SUM()` 用的,但不同聚合函数对帧边界的敏感度差异不小:
* `FIRST_VALUE(col) OVER (ORDER BY t ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)`:这个组合确实总能返回当前分区中 `t` 最小值那一行的 `col` 值。
* `MAX(col) OVER (...)`:在 `UNBOUNDED PRECEDING` 下,它等价于“到当前行为止的最大值”。但如果用了 `RANGE` 且 `ORDER BY` 列有重复,结果可能会在你期望之前就触发了更新。
* `LAST_VALUE(col)` 配合 `UNBOUNDED PRECEDING`要格外小心:它的默认帧是 `CURRENT ROW`,要拿到分区的最后一行,必须显式写成 `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING` 才行。
这里容易被忽略的一点是:帧定义一旦固定下来,就和聚合函数本身无关。`UNBOUNDED PRECEDING` 描述的是我们希望包含的行集合,而不是具体的计算逻辑。换了个函数,这个集合本身不会变;但这个集合是否合理,最终还是得看 `PARTITION BY` 和 `ORDER BY` 的配合是否妥当。