UNBOUNDED PRECEDING 是SQL窗口函数中表示“从当前分区第一行无条件起始”的特殊关键字,不可替换为0 PRECEDING;后者会被多数数据库解释为CURRENT ROW,导致逻辑错误且语法报错。

UNBOUNDED PRECEDING 是什么,为什么不能写成 0 PRECEDING
它不是“从第 0 行开始”,而是一个特殊标记,表示“从分区第一行无条件起始”。UNBOUNDED PRECEDING 和 UNBOUNDED FOLLOWING 是窗口帧边界的关键字,SQL 标准里它们不可替换为数字。写成 0 PRECEDING 会报错——比如 PostgreSQL 报 frame starting offset must not be negative,SQL Server 直接拒绝解析。
常见误用场景:想实现“到当前行为止的累计和”,却错误地写成 ROWS BETWEEN 0 PRECEDING AND CURRENT ROW。这其实等价于 ROWS BETWEEN CURRENT ROW AND CURRENT ROW(只算自己),因为 0 PRECEDING 在多数引擎中被解释为“当前行”,而非“首行”。
-
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW→ 正确的滚动汇总起点 -
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW→ 只含上一行 + 当前行(非滚动,是滑动) - 省略
ROWS子句时,部分数据库(如 MySQL 8.0+)默认行为是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,但语义不同(可能合并相同值的行)
滚动求和、平均、计数必须显式声明 ORDER BY,否则结果不可靠
窗口函数的排序逻辑决定“滚动方向”和“当前行位置”。没有 ORDER BY,CURRENT ROW 就失去参照系,数据库可能按任意物理顺序处理,导致每次查询结果不一致(尤其在并发或数据重分布后)。
例如对销售表按日期滚动加总:
SELECT sale_date, amount, SUM(amount) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumsum FROM sales;
如果漏掉 ORDER BY sale_date,即使数据暂时按日期插入,也不能保证 cumsum 正确。MySQL 会警告 Window 'w' with no ORDER BY is not allowed;PostgreSQL 虽允许,但行为未定义。
- 多列排序可写成
ORDER BY region, sale_date,此时滚动按 region 分组内再按日期展开 - 若需稳定排序但字段有重复(如多个同日订单),建议补一个唯一字段:
ORDER BY sale_date, order_id -
RANGE模式下,相同排序值的行会被归入同一帧——这对滚动平均可能意外拉高权重,慎用
UNBOUNDED 和 PARTITION BY 组合时,边界只在分区内生效
UNBOUNDED PRECEDING 不是“全表第一行”,而是“当前分区的第一行”。这是最容易忽略的语义点。一旦用了 PARTITION BY,每个分区独立计算滚动,互不影响。
典型例子:按员工部门做月度累计奖金
SELECT
dept,
month,
bonus,
SUM(bonus) OVER (
PARTITION BY dept
ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS dept_cum_bonus
FROM emp_bonus;
这里每个 dept 的累计从各自最早的 month 开始,而不是全表最早月份。如果某部门中间缺月,累计不会跳过——它只依赖已存在的行顺序。
- 分区字段类型要一致,避免隐式转换导致意外分组(如字符串
'01'和整数1被分到不同区) - 空值(
NULL)在PARTITION BY中会被归为同一组,但排序时默认排在最前或最后(依数据库而定),可能打乱预期顺序 - 若不需要分区,就别写
PARTITION BY——加上空分区(如PARTITION BY 1)看似“不分区”,实则引入额外哈希开销
性能敏感场景下,UNBOUNDED 配合大偏移量可能触发全分区扫描
虽然 UNBOUNDED PRECEDING 语义清晰,但它要求数据库维护从分区起点到当前行的完整状态。当分区极大(如单个用户千万级行为日志)、且 ORDER BY 字段无索引时,某些引擎(如旧版 Hive、Spark SQL 默认配置)会退化为每行遍历历史数据,O(n²) 时间复杂度。
优化线索看执行计划中的 windowagg 或 WindowNode 节点是否带 spill(落盘)或 high memory usage。
- 确保
ORDER BY字段有索引(PostgreSQL/MySQL)或已排序输入(Spark 中用DISTRIBUTE BY + SORT BY) - 避免在
UNBOUNDED PRECEDING窗口中使用高开销表达式(如嵌套子查询、UDF),它们会在每一行重复执行 - 如只需最近 N 行滚动,改用
ROWS BETWEEN N-1 PRECEDING AND CURRENT ROW,数据库更容易利用滑动窗口优化
真正难调试的不是语法,而是当分区逻辑和排序逻辑没对齐时,UNBOUNDED 看似在干活,其实累加了不该累加的行——得盯着 sample 数据和 PARTITION BY 输出一起验证。

















