不要用 limit offset, size 做深分页,因其需线性扫描并丢弃前 offset 行,i/o 与 cpu 开销随 offset 线性增长;应改用游标分页,基于排序字段的确定值(如 id > 6800000)高效分页。

直接结论:不要用 LIMIT offset, size 做深分页,它本质是线性扫描,偏移量越大越慢——这不是调优能解决的问题,是执行逻辑决定的。
为什么 LIMIT 1000000, 20 会卡住几秒?
MySQL 必须先读取并丢弃前 1000000 行,哪怕只返回 20 条。每行都可能触发回表(从二级索引查主键再到聚簇索引取数据),I/O 和 CPU 开销随 offset 线性增长。常见现象包括:
- 响应时间从毫秒级跳到秒级甚至超时
- 慢查询日志频繁出现
LIMIT N, M类语句 - 主从延迟加剧(大 offset 查询阻塞复制线程)
关键点:瓶颈不在内存或配置,而在磁盘遍历本身;ORDER BY 字段没索引、用了函数(如 DATE(created_at))、或排序字段不唯一,都会让优化器放弃索引走全表扫描。
游标分页:适用于“下一页/上一页”场景
把“跳过 N 行”改成“从某个确定值之后取”,彻底绕开 offset 扫描。要求排序字段有索引且可比较(如自增 id 或 created_at):
- id 升序分页:上一页最后
id是 6800000,下一页写SELECT * FROM t WHERE id > 6800000 ORDER BY id LIMIT 20 - 时间倒序分页:上一页最小
updated_at是'2026-04-10 15:22:33',下一页写SELECT * FROM t WHERE updated_at - 若
updated_at不唯一(多条同秒记录),必须补主键防漏/重:WHERE (updated_at, id)
注意:不能跳页(比如直接翻到第 100 页);ORDER BY 和 WHERE 字段需共用联合索引,例如 INDEX (status, created_at, id)。
延迟关联:支持任意页码跳转的折中方案
当业务必须允许用户输入页码(如后台管理),且无法改造成游标时,用子查询先捞 ID,再 JOIN 查详情:
SELECT a.* FROM orders a INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20 ) b ON a.id = b.id;
关键约束:
- 子查询里只选
id,确保走覆盖索引(如INDEX(status, created_at, id)) - 主表
a.id必须有索引(主键天然满足) - 禁止在子查询中写
SELECT *或带函数的ORDER BY - MySQL 5.6+ 对该写法优化较好,老版本需检查执行计划是否出现
Using index
这个方案把回表次数从 offset + size 次降到固定 size 次,但子查询本身仍要跳过大量索引项——所以它只是“比原写法快”,不是“快得离谱”。
容易被忽略的底层依赖
再好的写法也跑不赢糟糕的索引设计:
- 排序字段必须建索引,否则游标和延迟关联都会退化成全表扫描
- 避免在
WHERE或ORDER BY中对字段做计算,比如WHERE DATE(created_at) = '2026-01-01'会让索引失效 - 主键尽量用自增整型;UUID 会导致 B+ 树频繁分裂,范围查询效率下降
- 如果业务允许,后端应直接拒绝
offset > 10000的请求,前端加页码限制
真正难的不是写对 SQL,而是让游标值在前后端之间可靠传递、处理好边界条件(比如删除导致的空洞)、以及在时间字段不唯一时补主键的细节——这些地方一错,就漏数据或重复。











