游标分页是目前最轻量、最稳定、最推荐的方案,用 WHERE id > ? ORDER BY id LIMIT 20 替代 OFFSET,要求主键或排序字段单调递增且唯一,并确保 ORDER BY 与 WHERE 字段一致且有索引。

直接用 LIMIT offset, size 查第10万页?别试了,MySQL会扫描前100010行再丢掉99990条——这不是查询,是自我惩罚。
为什么 LIMIT 1000000, 20 会慢到超时?
MySQL没有“跳过N行”的底层能力。它必须从排序结果开头逐行读取,直到凑够 offset + size 行,然后扔掉前 offset 行。哪怕你只想要20条,它也得把前100万行全捞进内存、解包、再丢弃。
更糟的是:
- 如果排序字段没索引,触发
filesort,全表扫描+磁盘排序,秒变阻塞源 - 用二级索引排序后查
*,每行都要回表,100万次随机I/O,IO队列直接堆满 - InnoDB在扫描过程中可能持有间隙锁,高并发下锁竞争加剧
游标分页:用 WHERE id > ? ORDER BY id LIMIT 20 替代 OFFSET
这是目前最轻量、最稳定、最推荐的方案,前提是主键或排序字段单调递增且唯一。
核心逻辑不是“我要第N页”,而是“我要上一页最后一条之后的数据”。
- 第一轮查出最后一条的
id(比如15678),前端/客户端存下来 - 下一页请求带参
last_id=15678,后端生成:SELECT * FROM orders WHERE id > 15678 ORDER BY id LIMIT 20 - 必须确保
ORDER BY字段和WHERE条件字段一致,且该字段有索引(最好是主键) - 升序翻页用
>,降序翻页用;若需双向翻页,得同时传 <code>last_id和first_id
注意:删除中间记录会导致漏数据,但比卡死强得多;业务能接受“滚动加载”就别支持跳页。
延迟关联:当必须跳页(比如后台管理搜第500页)
用子查询先从索引里捞出目标页的主键,再用这些主键批量回表——把“扫描100万行+回表100万次”变成“扫描100万行索引+回表20次”。
典型写法:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY created_at DESC LIMIT 1000000, 20 ) AS tmp ON o.id = tmp.id;
关键点:
- 子查询里只选
id,确保走覆盖索引(比如INDEX(created_at, id)) - 外层
JOIN利用主键聚簇索引快速定位,避免全表扫 - 即使
offset是100万,子查询也只读20个索引节点(B+树深度通常就3~4层) - 不适用于
SELECT *且排序字段无索引的场景——那得先加索引
容易被忽略的硬约束
所有优化都建立在几个刚性前提上,漏掉任何一个,性能照样崩:
-
ORDER BY字段必须有索引,否则延迟关联和游标都无效 - 游标分页要求排序字段值严格单调(自增ID最稳,
created_at要配合id做联合条件防重复) - 不要在深分页场景里
SELECT COUNT(*),大表统计本身就会拖垮查询 - 前端禁止暴露“跳转到第N页”输入框,改用“加载更多”或“上一页/下一页”按钮
真正棘手的不是技术方案,而是说服产品放弃“总页数”和“页码跳转”——那才是深分页问题的根子。



















