mysql深分页性能退化源于limit需扫描offset+limit行再丢弃前offset行,导致i/o、cpu和内存开销线性增长,时间复杂度为o(offset+limit);书签法通过where+order by替代offset,实现恒定响应时间。

MySQL执行LIMIT时必须扫描前offset + limit行
不是“跳过前offset行再查”,而是先读满offset + limit行,再丢弃前offset行——这是性能退化的根源。比如LIMIT 100000, 20,MySQL实际要从索引中顺序读取100020条记录,哪怕只返回20条。
这个过程发生在服务器层,存储引擎(如InnoDB)按索引顺序逐条返回数据,执行器边收边计数,直到凑够offset + limit才停。中间所有被丢弃的100000条记录,仍消耗I/O、内存和CPU。
- 没有合适索引时,会触发全表扫描,代价更高
- 若
SELECT *且排序字段无覆盖索引,每条记录都要回表,随机I/O暴增 - MySQL优化器在偏移极大时可能放弃走索引,直接全表扫描(认为更省)
OFFSET越大,I/O和内存开销线性增长
时间复杂度是O(offset + limit),不是O(limit)。实测中:LIMIT 10, 20耗时0.016秒,LIMIT 400000, 20升至3.2秒,LIMIT 866613, 20达37秒——基本呈线性关系。
原因很实在:磁盘要读更多页,缓冲池要缓存更多临时结果,排序(如果没用索引排序)可能写临时文件,GC压力也会上升。
- 即使有
ORDER BY id且id是主键,只要offset超大,InnoDB仍得遍历B+树叶子节点,无法“跳转”到中间位置 - 使用
WHERE id > ? ORDER BY id LIMIT 20可绕过扫描,但前提是前端能维护上一页末尾的id值 - 复合索引如
INDEX (create_time DESC, id)能让子查询只走索引,避免回表,是延迟关联的前提
为什么优化器有时不走索引?
当MySQL估算出走索引需要回表10万次,而全表扫描只需顺序读几万页时,它可能判定后者更快——尤其在score这类低区分度字段上建单列索引后,深分页反而变慢。
这不是bug,是成本模型下的理性选择。你看到执行计划里type: ALL而不是range,往往就是这个原因。
- 用
EXPLAIN FORMAT=JSON看query_cost,对比不同写法的实际开销 -
FORCE INDEX能强制走索引,但可能更慢,慎用 - 真正可靠的解法不是逼它走索引,而是改写SQL,让“跳过”动作消失
书签法(游标分页)为什么稳定?
因为它彻底去掉了OFFSET——把“第N页”变成“从某条记录之后取20条”。只要上一页最后一条的create_time和id已知,下一页就是:
SELECT * FROM orders WHERE create_time <p>这种写法永远只查20条,响应时间恒定,但代价是不能任意跳页,且要求排序字段组合能唯一标识一行。</p>
- 业务允许“下一页/上一页”但不需要“跳到第100页”时,书签法是最优解
- 时间字段精度不足(如只到秒)时,必须补上主键做第二排序条件,否则漏数据或重复
- 千万级表上,
LIMIT 1000000, 20和LIMIT 20耗时差异可达百倍,但书签法两者无差别
真实场景里,深分页慢不是配置或版本问题,是SQL语义本身带来的必然开销。换思路比调参数管用得多。











