ROW_NUMBER()本身不解决深度跳页性能问题,真正起效需排序走索引、过滤尽早发生、中间结果集可控;否则易现Using temporary/filesort、页码越大越慢、编号不稳定或查不到数据等问题。

ROW_NUMBER() 本身不解决深度跳页性能问题,它只是个编号工具;真正起效的前提是:排序走索引、过滤尽早发生、中间结果集可控。
为什么直接套 ROW_NUMBER() 反而更慢
常见错误现象:EXPLAIN 显示 Using temporary; Using filesort,执行时间随页码线性增长,第 1000 页比第 1 页慢 50 倍。
- ORDER BY 字段没索引 → 引擎必须全表排序再编号,I/O 和内存双爆
- WHERE 条件写在窗口外(比如 CTE 外层)→ 窗口先算全表,再过滤,内存占用翻倍
- 排序列不唯一(如只写
ORDER BY create_time DESC)→ 同一时间戳的行编号不稳定,翻页漏数据或重复 - 用
PARTITION BY做全局分页 → 行号按组重置,rn BETWEEN 100001 AND 100020根本查不到东西
必须配的索引结构
索引不是“加一个就行”,必须和 OVER (ORDER BY ...) 完全对齐,并覆盖高频过滤字段。
- 若写
ROW_NUMBER() OVER (ORDER BY create_time DESC, id ASC),就要建INDEX (create_time DESC, id ASC) - 若 WHERE 常带
status = 'shipped',索引应前置该列:INDEX (status, create_time DESC, id ASC) - 避免在
ORDER BY里用表达式,比如ORDER BY DATE(created_time)→ 索引失效,必然触发磁盘排序
CTE 写法中容易踩的坑
CTE 是推荐结构,但细节错一点就回到原点。
-
WHERE必须写在 CTE 内部(即SELECT ... FROM orders WHERE status = 'shipped'),不能留到外层 -
rn BETWEEN X AND Y是安全写法;WHERE rn > 1000000 LIMIT 20在 MySQL 中语法报错,SQL Server 虽允许但执行计划极差 - 页码转范围时注意整数溢出:
($page - 1) * $size + 1在$page = 100000、$size = 10000时会超BIGINT上限 - 别把视图名或排序字段拼进 SQL 字符串——必须用
QUOTENAME()(SQL Server)或参数化(MySQL)防注入
深度跳页(> 10 万偏移)其实不该用 ROW_NUMBER()
当你要支持 page=5000 这种随机跳转时,ROW_NUMBER() 和 LIMIT OFFSET 都已不是最优解。
- 两者都需完整排序 + 全量编号/跳过,复杂度仍是 O(N),只是常数项略低
- 真正扛住千万级的,是游标分页(Keyset Pagination):靠上一页最后一条的
(created_at, id)值做WHERE条件,查询复杂度稳定在 O(log N) - 如果业务强制要页码输入(比如后台系统),且数据量 ≤ 10 万行,老老实实用
LIMIT OFFSET+ 正确索引,反而更轻量、更可预测


















