性能问题背后的真相:UNBOUNDED PRECEDING 为什么这么“贵”?
先说几个核心判断:UNBOUNDED PRECEDING 带来的性能代价,远不止“慢一点”那么简单。
它真正可怕的地方在于,会迫使数据库放弃流式计算,转而把整个分区的数据缓存下来,再逐行处理。只要你的窗口定义里含了 UNBOUNDED PRECEDING 并且带了 ORDER BY,引擎就必须保证每一行都能访问从分区首行到当前行的全部数据——这恰巧就击碎了增量聚合的前提。
你可以通过一些运行时的蛛丝马迹来确认是否“中招”:比如 WindowAgg 节点的 Actual Rows 是输入行数的 N 倍(N 也就是分区数),Buffers: shared read 猛增,Sort Method: external merge 频繁出现。具体到各家数据库的表现:
- PostgreSQL 在
work_mem不够时会疯狂写临时文件,temp_buffers占用甚至可以达到 GB 级 - SQL Server 的
tempdb日志写入量暴涨,往往伴随着sort warning事件 - MySQL 8.0.17 之前,实际上退化为
O(n²)的关联模拟来跑,就算有索引也很难规避

ROWS 和 RANGE 下的 UNBOUNDED PRECEDING,性能落差极大
这里要特别注意的是 ROWS 和 RANGE 的区别。ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 至少还能按物理行偏移跳指针;但默认的 RANGE 帧在排序键有重复值时,会动态扩展窗口——比如按 sale_date 排序,同一天有 1000 笔订单,那么第 1 行的窗口就已经包含了全部 1000 行,后续每一行都得从头重扫一遍。
实测下来,同样的查询,RANGE 比 ROWS 要慢上 5 到 10 倍,尤其在排序字段基数很低(比如状态码、枚举这类)的时候,效果更明显。
那该怎么决策呢?几条实用的经验:
- 只有业务明确需要“同值同处理”(比如并列排名)时,才去用
RANGE,否则一律显式写成ROWS - 时间序列场景要格外小心
RANGE BETWEEN INTERVAL '7' DAY PRECEDING,重复日期很容易让窗口失控 - 各家数据库对
RANGE的支持程度不一:MySQL 支持有限,PostgreSQL 允许INTERVAL,SQL Server 则不支持日期型的RANGE——在做跨库迁移的时候,默认帧极容易出错
ORDER BY 字段没有索引,会让 UNBOUNDED PRECEDING 彻底失控
引擎没办法流式读取,只能先把全部数据拉进内存或磁盘里排好序。这种情况下,UNBOUNDED PRECEDING 带来的已经不是语义问题,而是物理执行层面的灾难:排序本身是 O(n log n),每行再累计一下,成了 O(n),最坏的路径逼近 O(n²)。
几条可以立刻上手的优化建议:
- 对高频率使用的
PARTITION BY字段(比如user_id),建一个 B-tree 索引,哪怕只有一个字段,也能触发 merge-join 路径 - 如果
ORDER BY是一个表达式(比如date_trunc('month', created_at)),那必须建函数索引:CREATE INDEX idx_orders_month ON orders (date_trunc('month', created_at)) - 尽量避免
ORDER BY UPPER(name)这种没法走普通索引的写法;如果实在要用,得配合函数索引,并且仔细评估代价
一个容易被忽视的语义陷阱
必须警惕的是:UNBOUNDED PRECEDING 并不等于“从表头开始”。它永远只作用于当前分区,不是整张表的第一行。如果 PARTITION BY dept_id,那每个部门各自从自己分区的第一行算起——但如果忘了写 PARTITION BY,又写了 ORDER BY ts,数据库就会把整张大表当作一个分区,UNBOUNDED PRECEDING 就真成了“从亿级表的第一行扫到当前行”。
还有一种更隐蔽的情况:没有写 FRAME 子句时,有 ORDER BY 则默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,没有 ORDER BY 则默认是 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING——后者在 ROW_NUMBER() 这种场景下也会强制全表排序。
所以真正的关键不是要不要删掉 UNBOUNDED PRECEDING,而是确认它是否真的需要覆盖整个分区。很多累计需求实际上只需要最近 N 行数据,ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 往往才是更可控、也更理智的选择。
