根本原因是row_number()强制全量排序编号后过滤,无法跳过前n行;需索引优化、精简字段、优先用游标分页。

ROW_NUMBER() 为什么一查大页就卡
根本原因不是函数本身慢,而是它强制数据库先完成全量排序+编号,再过滤。哪怕你只要第100万行后的10条,ROW_NUMBER() 也得把前100万行全排完、全编号完——这过程无法跳过,也不能用索引 Seek 跳转。
常见错误现象包括:翻到第5万页后查询从20ms飙升到2秒以上;EXPLAIN 显示“WindowAgg”节点逻辑读暴增;SQL Server 中看到大量 PAGLOCK 或 KEYLOCK 等待。
- 排序字段没索引 → 全表扫描 + 内存外排序(spill to tempdb)
- 子查询里写
SELECT *→ 临时结果集体积膨胀,拖慢编号速度 - 用
updated_at单字段做排序 + 分页 → 同一秒多条记录导致rn不稳定,分页错位 - 在 GaussDB / Hive 等引擎中,窗口函数会打断流水线执行,必须等上层算子全部产出才往下传
什么时候该放弃 ROW_NUMBER() 改用 OFFSET FETCH
SQL Server 2016+ 和 MySQL 8.0.2+ 都支持 OFFSET ... FETCH,它在有合适索引时能走 “Top-N Sort”,跳过部分排序开销。但这只在特定条件下真正有效:
- 排序字段有**高选择性唯一索引**(如
id或(status, updated_at, id)组合) -
FETCH NEXT行数 ≤ 100(越大越容易退化为全排序) - 不依赖行号做业务判断(比如不能靠
rn = 1标记“最新一条”) - 你不需要跨页随机跳转,而是顺序翻页(否则游标分页更稳)
反例:ORDER BY updated_at DESC OFFSET 999999 ROWS FETCH NEXT 20 ROWS ONLY —— 如果 updated_at 有重复,执行计划仍可能回退成全排序,且逻辑读是游标的3–5倍。
游标分页比 ROW_NUMBER() 快的本质原因
游标分页(Keyset Pagination)不依赖行号,而是用上一页最后一条的主键/组合键值作为下一次查询的起点,例如:
SELECT id, title, updated_at FROM posts WHERE status = 1 AND (updated_at, id) <p>这个查询能直接命中索引 B+ 树的某个叶子节点位置,完全避免排序和编号。但要注意:</p>
- 必须用**复合条件**兜住重复排序值,单用
updated_at 会丢数据 - 索引要按排序顺序覆盖所有参与比较的列,否则会回表或失效
- 前端必须保存上一页末尾的完整排序键(不止时间,还得带
id) - 数据实时更新时,新插入记录可能“插队”,导致某条记录被跳过(这是语义一致性的权衡,不是 bug)
ROW_NUMBER() 还不得不留的三个真实场景
不是所有分页都能换成游标。以下情况 ROW_NUMBER() 仍是合理选择,但必须配合约束使用:
- 需要“第 N 页的第 M 条”这种绝对位置语义(比如后台管理导出指定页码)
- 排序逻辑含表达式或联表字段(如
ORDER BY u.name COLLATE utf8mb4_unicode_ci),无法建高效索引 - 分组内编号,例如“每个 category 下销量 Top 3 的商品”:
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY sales DESC)
此时性能关键在:子查询只选必要字段 + PARTITION BY 列和 ORDER BY 列联合建索引 + 避免在窗口函数外再套聚合或 JOIN。GaussDB 等引擎中,这类写法若没压平执行流,rn = 1 过滤点实际会等整个分区编号完成才触发,延迟明显。











