MySQL深分页越翻越慢本质是LIMIT offset,size需扫描并丢弃offset行,非SQL错误而是设计使然;推荐子查询+主键关联或游标分页(WHERE id > ?),要求排序字段有索引且避免SELECT *。

MySQL大表分页越翻越慢,本质不是SQL写得不对,而是LIMIT offset, size在深层偏移时必须扫描并丢弃大量数据——这不是bug,是设计使然。只要用LIMIT 1000000, 20这类写法,就注定要扫1000020行、回表1000020次、传输/丢弃100万行无用数据。
为什么LIMIT offset, size会随页码线性变慢
MySQL不会“跳转”到第N行,它从索引起点开始逐行计数:扫1行→+1,扫到第offset+1行才开始取数据。即使你只想要20条,前100万行也得完整走一遍B+树节点、做比较、进缓冲区、再丢弃。
-
EXPLAIN里rows显示值 ≈offset + size,这就是真实扫描量 - 如果
ORDER BY字段没索引,还会触发Using filesort,磁盘排序直接拖垮性能 - 查
SELECT *时,每丢弃一行都要加载整行数据(含大字段),IO和内存压力倍增 - 主键非自增或有空洞时,基于ID的简单大于查询可能漏数据,不能直接套用
延迟关联法:最通用、改动最小的优化方案
核心是把“全字段扫描+丢弃”拆成两步:先用覆盖索引极速捞出需要的id,再用这些id精准回表。子查询不碰大字段,外层只查20次主键。
SELECT t1.* FROM orders t1 JOIN (SELECT id FROM orders ORDER BY create_time DESC LIMIT 1000000, 20) t2 ON t1.id = t2.id;
- 要求
ORDER BY字段(如create_time)必须有索引,且最好是联合索引覆盖排序+查询条件 - 子查询
SELECT id能走覆盖索引,rows仍为1000020,但只读索引页,不加载数据页 - 外层
JOIN是等值主键查找,每次都是O(log n),20次就是20次快查 - 比原始语句快5–50倍,且对业务代码侵入极小,DAO层基本不用改
基于游标的分页:WHERE ... AND ... ORDER BY替代LIMIT offset
放弃“第N页”的思维,改用“下一页从哪开始”。客户端记住上一页最后一条的排序字段值(如create_time和id),下次请求带过去。
SELECT * FROM orders WHERE create_time < '2026-08-20 14:30:00' OR (create_time = '2026-08-20 14:30:00' AND id < 8892345) ORDER BY create_time DESC, id DESC LIMIT 20;
- 必须用
ORDER BY字段做条件,且该字段要有索引;复合排序时,条件也要复合(时间相同则比ID) - 不能跳页(比如从第1页直接跳第100页),但适合“下一页”按钮场景,性能稳定在毫秒级
- 时间字段重复率高时,
id是必要兜底,否则会漏或重 - 注意时区和精度:
DATETIME(3)字段传参必须带毫秒,否则条件失效
容易被忽略的关键细节
所有优化都依赖索引有效性,而索引失效往往藏在细节里:
-
ORDER BY create_time DESC,但索引是INDEX(create_time)(默认ASC)——不匹配,无法利用索引排序 - 查询条件含函数,如
WHERE DATE(create_time) = '2026-09-01',索引失效 - 字符串字段用数字比较,如
WHERE status = 1而status是VARCHAR,触发隐式转换,索引失效 - 分页接口被前端暴力刷页(如并发拉1000页),即使优化后也会压垮数据库——需配合缓存或限流



















