row_number() 分页比 offset fetch 更快,因其通过子查询+主键过滤利用索引快速定位起始位置,避免扫描丢弃大量行;但必须将 where 条件置于子查询内、order by 末尾补唯一列保证稳定性,并在跳页场景切换为游标分页。

ROW_NUMBER() 分页为什么比 OFFSET FETCH 更快?
在数据量大、偏移量高(比如 OFFSET 100000)时,OFFSET FETCH 仍需扫描前 N 行再丢弃,I/O 和 CPU 开销陡增;而 ROW_NUMBER() 配合子查询 + 主键过滤,能利用索引快速定位起始位置,尤其当排序字段有索引且分页条件可下推时,性能差距可达数倍。
但注意:它不是“无代价优化”——ROW_NUMBER() 本身需对整个结果集排序并编号,若不加限制,全表打序照样慢。关键在于**把 ROW_NUMBER() 套在最小必要结果集上**。
必须带 WHERE 的子查询包裹 ROW_NUMBER()
直接写 SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM orders 再外层过滤 rn BETWEEN 10001 AND 10020 是典型错误:引擎大概率先算出全部行的 rn,再筛,失去意义。
正确做法是把业务过滤条件提前压入子查询,让排序和编号只作用于目标数据子集:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn FROM orders WHERE status = 'shipped' -- ✅ 这里过滤掉 95% 的无效行 ) t WHERE t.rn BETWEEN 10001 AND 10020;
- WHERE 必须在子查询内,不能只放在外层
- ORDER BY 字段最好覆盖查询条件中的高频筛选字段(如本例中
status虽未出现在 ORDER BY,但因有索引,配合 WHERE 可走索引范围扫描) - 若排序字段无索引,
ROW_NUMBER()会强制 Sort 算子,此时不如改用游标分页
ORDER BY 必须唯一,否则分页结果不稳定
ROW_NUMBER() 在排序值相同时按任意顺序编号,导致同一页反复刷新出现重复或遗漏行。例如按 created_at 排序,若多条记录时间戳完全相同,下次查询可能编号错位。
解决方法:在 ORDER BY 末尾补一个唯一列(通常是主键),确保排序确定性:
ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC)
- 不要用
ORDER BY created_at DESC, NEWID()—— 随机值破坏可重现性,分页失效 - 如果业务允许,优先用
id或rowid这类天然唯一且有序的列 - 联合索引应匹配该顺序,例如
INDEX idx_created_id (created_at, id)
跳页场景下,用上一页最大/最小值做游标更稳
用户跳到第 500 页(rn BETWEEN 49901 AND 49920)时,子查询仍要算出近 5 万行编号,性能回落。这时 ROW_NUMBER() 已非最优解。
真实生产环境推荐混合策略:前几页用 ROW_NUMBER(),之后切换为基于游标的分页(即记录上一页最后一条的 created_at 和 id,用 WHERE (created_at, id) 查询下一页)。
这要求前端保存游标值,后端改写 SQL,但避免了深度分页的排序膨胀问题。很多团队卡在“只认一种分页写法”,其实 ROW_NUMBER() 是利器,不是银弹。










