MySQL深度分页慢的本质是LIMIT offset, size需扫描并丢弃offset行,导致扫描行数随偏移量线性增长;优化方案包括游标分页(基于有序字段范围查询,复杂度O(log n))、延迟关联(子查询索引覆盖取id,外层主键回表)和范围分页(需唯一单调字段,不支持跳页)。

MySQL执行LIMIT时根本不会“跳过”前N行
它老老实实扫描、排序、计数,直到凑够LIMIT offset + size行,再把前offset行全扔掉。哪怕你只想要最后20条,它也得先把前100万行都拉出来——每行都可能触发回表、磁盘排序或临时文件写入。
常见错误现象:EXPLAIN里rows值巨大,Extra列出现Using filesort或Using temporary;并发查几页后,innodb_buffer_pool_reads飙升,磁盘IO打满。
- ORDER BY字段没索引?直接全表扫描+外部排序,性能断崖下跌
- WHERE条件用了
IN、函数或模糊匹配?索引失效,优化器被迫放弃有序遍历 - 排序字段和查询条件不在同一复合索引里?B+树无法同时满足过滤和排序,只能选其一甚至全扫
延迟关联(Deferred Join)怎么写才不翻车
核心是把“找id”和“取数据”拆开:子查询只走索引捞id,外层用主键精准回查。但写错一行就退化成更慢的全表扫描。
正确写法必须满足三个硬条件:
- 子查询里
SELECT只能是id(或联合索引覆盖的字段),不能SELECT *或加其他列 -
ORDER BY字段必须有对应复合索引,且顺序匹配,比如WHERE status = 1 ORDER BY created_at DESC,索引就得是INDEX(status, created_at, id) - 外层
JOIN必须用ON t1.id = t2.id这种等值主键连接,换成IN或范围条件,MySQL 5.7+ 也可能放弃索引下推
示例:
SELECT t1.* FROM orders t1 INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20 ) t2 ON t1.id = t2.id;
游标分页为什么比LIMIT更稳,但容易漏数据
它用上一页末尾记录的排序值当起点,比如WHERE id ,直接定位B+树叶子节点,时间复杂度稳定在<code>O(log n)。
但业务逻辑稍不注意就会出问题:
- 删除中间某条记录?下一页会跳过紧随其后的那条(漏数据)
- 并发插入新记录到已翻过的页范围内?同一条可能被重复返回
- 用
created_at做游标却没加id二级排序?同一秒内多条记录会导致顺序不稳定,翻页错乱 - 客户端没保存好上一页末尾的
(created_at, id)值?游标失效,结果不可控
典型安全写法:
SELECT * FROM orders WHERE (created_at, id) < ('2024-01-01 10:00:00', 999999) ORDER BY created_at DESC, id DESC LIMIT 20;
复合索引建错顺序,优化直接白干
索引字段顺序不是随便排的。WHERE条件字段要放最左,排序字段紧跟其后,最后补上主键id——否则子查询无法走覆盖索引,游标条件也无法高效定位。
比如这个查询:
SELECT * FROM orders WHERE status = 1 AND category IN ('A','B') ORDER BY created_at DESC;
错误索引:INDEX(created_at, status)(排序字段在前,WHERE字段无法利用最左前缀)
正确索引:INDEX(status, category, created_at, id)(IN字段放=字段之后,created_at用于排序,id确保子查询覆盖)
如果category是范围查询,created_at就无法继续走索引排序——这时得换思路,比如先用status过滤出小结果集,再内存排序。
真正卡住人的从来不是语法,而是索引能否让WHERE和ORDER BY同时生效,以及业务是否允许放弃“跳转任意页”来换取稳定响应。


















