mysql深分页慢的根本原因是innodb必须逐行扫描并跳过前offset行,无法直接定位,且回表次数≈offset+limit,导致大量无效io和cpu开销。

深分页时InnoDB必须回表+逐行跳过,不是“跳到第N页”
MySQL执行 LIMIT 1000000, 20 这类深分页时,InnoDB**不会直接定位到第1000001行所在的页**,而是老老实实从聚簇索引起点开始扫描、计数、跳过前100万行——哪怕只取20条结果。这是由B+树的遍历机制和MySQL执行器的协作方式决定的。
关键点在于:InnoDB没有“随机访问第N行”的能力,它的页间链表(FIL_PAGE_PREV/FIL_PAGE_NEXT)只支持顺序遍历,不支持O(1)偏移寻址。
二级索引+ORDER BY + LIMIT 的执行路径最典型
假设你用的是 SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC LIMIT 1000000, 20,且有联合索引 idx_user_time (user_id, create_time):
- 先走二级索引
idx_user_time找到所有user_id = 123的主键值(可能上万条) - 按
create_time DESC排序这些主键(注意:二级索引叶子节点里主键值本身是无序的,排序发生在内存或临时表) - 取排序后第1000001~1000020个主键值
- 对这20个主键,逐一回表到聚簇索引查完整行——但为了拿到这20个主键,前面100万次回表已经悄悄发生了(因为MySQL要先确认哪20个主键在“跳过”之后的位置)
也就是说:**回表次数 ≈ 跳过的行数 + 返回的行数**,不是只回表20次。这也是为什么深分页慢得离谱——大量无效IO和CPU排序开销全白花了。
聚簇索引主键自增场景下也逃不开线性扫描
即使你用的是 SELECT * FROM t ORDER BY id LIMIT 1000000, 20,且 id 是自增主键:
- InnoDB仍需从B+树最左叶子页开始,沿着页的双向链表(
FIL_PAGE_NEXT)一页一页往下读 - 每页内再通过页目录(Page Directory)做二分查找定位记录,但跨页仍是顺序的
- 它无法根据“第1000001行”反推出该行落在哪个物理页——因为页内行数不固定(受行大小、删除、填充因子影响),页之间也不保证连续物理存储(区Extent可能碎片化)
所以即使没有WHERE条件,只要用了 LIMIT offset, size,offset越大,扫描的页数和记录数就越多,性能越接近全表扫描。
真正被忽略的代价:页分裂残留和区碎片让深分页更慢
生产环境的表往往经历过大量写入/删除,导致:
- 页内空闲空间(Free Space)分布不均,页目录稀疏,单页内查找效率下降
- 区(Extent)碎片化严重,原本连续的64页(1MB)变成散落在磁盘各处,预读(read-ahead)失效,每次读页都变成随机IO
- 如果曾经用过UUID主键插入,B+树非叶子层可能更深,导致每次回表要多走几层索引页
这些底层问题不会报错,也不会出现在慢查询日志里,但会让同样 LIMIT 1000000, 20 的查询在不同时间、不同实例上耗时波动极大——这才是最头疼的地方。











