ROW_NUMBER()分页优于OFFSET FETCH因能先过滤再编号,提升大数据量偏移性能;需稳定ORDER BY、索引支持,适用于跳转分页而非游标分页,CTE写法最清晰兼容。

ROW_NUMBER() 分页为什么不能直接用 OFFSET FETCH?
因为 OFFSET FETCH 在大数据偏移时性能急剧下降(比如跳到第 100 万页),而 ROW_NUMBER() 配合子查询或 CTE 能让数据库更早过滤行,尤其在有合适索引时,执行计划更容易走索引扫描+TOP N。
但注意:它不是“替代 OFFSET FETCH 的万能方案”,而是针对「稳定排序+已知页码」场景的可控优化手段。
- 必须有确定的
ORDER BY,否则ROW_NUMBER()结果不可重复 - 排序字段最好有索引,否则每次都要全表排序,开销反而更大
- 不能用于游标式分页(如“下一页从上页最后一条之后开始”),只适合“第 N 页,每页 M 条”这种跳转分页
标准写法:CTE + ROW_NUMBER() + BETWEEN
这是最清晰、兼容性最好的写法,适用于 SQL Server、PostgreSQL、Oracle(12c+)、MySQL 8.0+。
WITH paged AS ( SELECT *, ROW_NUMBER() OVER (ORDER BY id DESC) AS rn FROM orders WHERE status = 'shipped' ) SELECT id, order_date, amount FROM paged WHERE rn BETWEEN 21 AND 30;
查第 3 页(每页 10 条)就对应 rn BETWEEN 21 AND 30。关键点:
-
WHERE条件(如status = 'shipped')必须放在 CTE 内部,先过滤再编号,避免给无用行计算序号 -
ORDER BY中的字段(这里是id DESC)必须和业务排序逻辑一致,否则前端看到的“第 3 页”和用户预期不符 - 如果
id有重复值,需加唯一键补排序,例如ORDER BY id DESC, created_at DESC, order_id
SQL Server 特别注意:不要用子查询嵌套太深
在 SQL Server 中,把 ROW_NUMBER() 放进多层子查询(比如三层嵌套)可能让优化器放弃并行计划,导致性能反降。常见错误写法:
SELECT * FROM (
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM orders
) t1 WHERE rn > 20
) t2 WHERE rn <= 30;这比 CTE 写法更容易触发低效执行计划。建议统一用 CTE,或者改用 OFFSET FETCH(如果页码靠前、数据量不大)。
- SQL Server 2012+ 支持
OFFSET FETCH,小偏移(OFFSET )时它更简洁且优化器更友好 - 若坚持用
ROW_NUMBER(),确保外层WHERE条件能下推——检查执行计划里是否有 “Compute Scalar” 后紧跟 “Filter”,而不是 “Filter” 在最外层
MySQL 8.0 / PostgreSQL 的一个隐藏坑:NULL 值影响排序稳定性
当 ORDER BY 字段含 NULL,不同数据库对 NULL 的默认排序位置不同(MySQL 默认 NULL 最小,PostgreSQL 默认 NULL 最大),会导致同一语句在不同库中 ROW_NUMBER() 编号顺序不一致。
解决方法是显式声明 NULLS FIRST 或 NULLS LAST(PostgreSQL 支持;MySQL 8.0 不支持该语法,需用 IFNULL() 或 COALESCE() 转换):
-- PostgreSQL 安全写法 ROW_NUMBER() OVER (ORDER BY updated_at DESC NULLS LAST, id DESC)
-- MySQL 8.0 等效写法 ROW_NUMBER() OVER (ORDER BY IFNULL(updated_at, '1970-01-01') DESC, id DESC)
漏掉这点,上线后跨库迁移或主从切换时,分页结果错位很难排查。
真正麻烦的从来不是写对第一版,而是排序字段出现空值、索引失效、或页码跳得太大时,ROW_NUMBER() 会默默变慢,而日志里看不出异常。

















