ROW_NUMBER()在并行计划中反而更慢,根本原因是窗口函数要求“分区完成后再编号”,导致并行粒度错配:高基数分区(如百万user_id)触发海量轻量排序任务争抢资源,低区分度分区(如status仅2值)则撑爆sort buffer引发磁盘溢出;同时索引必须严格按PARTITION BY+ORDER BY字段顺序创建,否则失效;游标分页可规避编号开销,但需高选择性排序键和前端保存完整键值。

ROW_NUMBER() 为什么在并行计划里反而更慢
根本原因不是并行本身不好,而是窗口函数强制要求“分区完成后再编号”,导致并行粒度错配。数据库调度器可能为每个小 PARTITION BY 值单独分配线程,结果是成千上万个轻量排序任务争抢 CPU 调度和内存 buffer,反不如单线程大排序高效。
- 分区字段基数太高(如
user_id百万级唯一值)→ 生成百万个独立排序上下文,线程创建/销毁开销压倒收益 - 分区字段低区分度(如
status IN ('active', 'inactive'))→ 两个超大分区撑爆 sort buffer,频繁 spill 到磁盘 - MySQL 8.0+/PostgreSQL 的并行窗口排序只在分区数据量 > 几万行且内存充足时才真正启用;小分区下调度损耗更大
-
EXPLAIN中看到大量WindowAgg (parallel)节点但实际执行时间飙升,基本可判定是粒度失配
索引必须严格匹配 PARTITION BY + ORDER BY 顺序
窗口函数不走普通查询索引,它需要能直接支撑“分区内快速排序”的复合索引。漏掉任一字段或顺序错位,就等于没建。
- 写法:
ROW_NUMBER() OVER (PARTITION BY dept_id, team_id ORDER BY updated_at DESC, id DESC) - 对应索引必须是:
CREATE INDEX idx_dept_team_updated_id ON t(dept_id, team_id, updated_at DESC, id DESC) - 只建
(updated_at)或(dept_id, updated_at)→ 无效,仍需回表+全量排序 - ORDER BY 含函数(如
UPPER(name))→ 索引完全失效,必然文件排序
用游标分页替代 ROW_NUMBER() 分页
当你要的只是“第 N 页的 20 条”,而不是“每条记录的绝对行号”,ROW_NUMBER() 就是过度计算。游标分页跳过所有编号动作,直接定位。
- 关键条件:排序字段必须有高选择性,且前端能保存上一页末尾的完整排序键(不止时间,还得带
id) - 安全写法:
WHERE (updated_at, id) - 单用
updated_at → 同一秒多条记录时会丢数据或重复 - 索引必须覆盖全部比较列:
(updated_at DESC, id DESC),否则无法跳过扫描
改写为聚合 + 关联替代窗口函数
如果业务只要“每个分组最新一条”,而非“所有行加序号”,ROW_NUMBER() 就是杀鸡用牛刀。聚合方案让算子流水执行,避免窗口函数阻塞。
- 原写法:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY prod_name ORDER BY run_date DESC) rn FROM t) WHERE rn = 1 - 改写后:
WITH latest AS (SELECT prod_name, MAX(run_date) max_date FROM t GROUP BY prod_name) SELECT t.* FROM t JOIN latest ON t.prod_name = latest.prod_name AND t.run_date = latest.max_date - 优势:无窗口函数,
GROUP BY和JOIN可并行,且MAX()走索引极快 - 注意:若
run_date不唯一,需补id做第二排序,否则可能取到多条
游标分页和聚合改写能绕过窗口函数的底层执行模型,这是最彻底的解法。但容易被忽略的是:排序键的稳定性必须由业务语义兜底——比如用 id 消除时间重复,而不是指望数据库自动保证“最新”含义。


















