mysql深分页慢的根本原因是需扫描并丢弃前offset行,无法跳过索引或避免回表;游标分页通过“大于上一页末id”替代offset,消除偏移代价。

MySQL深分页为什么慢:从执行计划看offset的代价
MySQL深分页(比如 LIMIT 1000000, 20)慢,根本原因不是“取20条”,而是必须先扫描并丢弃前1000000行——哪怕这些行最终完全不返回。InnoDB引擎在执行时会逐行读取、校验、计数,直到跳过指定offset数量的满足WHERE条件的记录,这个过程无法跳过索引节点,也无法利用覆盖索引规避回表。
常见错误现象:EXPLAIN显示rows高达百万级,但Extra字段里却写着Using where; Using filesort或空着,容易误判为“只是排序慢”。实际上,只要offset大,即使有索引、无排序、无JOIN,性能也必然陡降。
用游标分页(Cursor-based Pagination)替代OFFSET
本质是把“我要第N页”换成“我要比上次最后一条更大的下20条”,彻底消除offset计算。前提是排序字段(如created_at、id)严格唯一且有索引。
- 原写法(危险):
SELECT * FROM orders WHERE status = 'paid' ORDER BY id DESC LIMIT 999980, 20 - 改写后(安全):
SELECT * FROM orders WHERE status = 'paid' AND id (其中<code>12345678是上一页最后一条的id) - 注意:必须用
而非<code>,否则可能重复;若排序字段非唯一(如多个同<code>created_at),需追加二级排序键(如id)保证确定性 - 首次请求不能用游标,仍需
LIMIT 0, 20,但只发生一次,后续全靠游标推进
强制索引+覆盖索引能缓解但不能根治OFFSET问题
如果必须用OFFSET(如后台管理页导出),可通过减少回表和IO来压低单次耗时,但offset本身的线性扫描成本仍在。
- 确保
ORDER BY字段在索引最左位,且WHERE条件能命中同一索引(例如INDEX(status, created_at, id)) - 用覆盖索引让查询只走索引树:
SELECT id, created_at, status FROM orders WHERE status = 'paid' ORDER BY created_at DESC LIMIT 1000000, 20,避免SELECT *触发大量回表 - 不要依赖
FORCE INDEX强行走某个索引——优化器选错索引往往说明索引设计本身有问题,优先重构索引 - 注意:即便用了覆盖索引,
offset = 1000000仍要遍历1000000+20个索引项,只是不读数据页而已
用延迟关联(Deferred Join)减少主表扫描量
适用于需要查完整行、又无法改成分页逻辑的场景。核心思想是“先用索引捞出ID,再按ID回查”,把大偏移量操作限制在窄索引上。
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 1000000, 20
) AS tmp ON o.id = tmp.id;
这里子查询只扫描索引(假设id在索引中),tmp结果集只有20个id,外层JOIN只需20次主键查找。相比原查询扫描百万行再取20条,IO大幅下降。但要注意:子查询仍需跳过1000000行,只是每行只读索引字段,速度更快;若id不在索引中(比如没建status + created_at + id联合索引),该优化无效。
真正难处理的是排序字段和过滤字段无法被同一个索引覆盖、又必须支持任意页码跳转的场景——这时要么接受慢,要么加缓存预生成分页结果,没有银弹。











