mysql深分页limit offset本质是顺序扫描并丢弃前offset行,无法跳过,即使加索引或换ssd也难根治;游标分页(where > last_value)和延迟关联可优化,但业务若强制要求任意页码跳转,性能瓶颈仍存在。

MySQL执行LIMIT OFFSET时必须扫描前offset+size行
这不是优化不到位的问题,而是MySQL执行模型的固有行为:LIMIT 1000000, 20 并不会跳过前100万行,而是从索引起点开始,逐行读取、计数,直到累计读满1000020行,再丢弃前100万行——被丢弃的行一样要走索引查找、一样可能触发回表、一样消耗I/O和CPU。
常见错误现象:EXPLAIN 中 rows 值等于 offset + size;Handler_read_next 指标暴涨;慢查询日志里同一SQL在页码靠后时耗时从几毫秒升至数秒甚至超时。
即使加了覆盖索引,也只减少回表开销,无法绕过“扫描+丢弃”这个步骤。B+树索引不支持“跳过N个节点”,它只能顺序遍历叶子链表。
为什么加索引或换SSD解决不了根本问题
索引能加速单行定位,但对OFFSET无效:排序字段有索引,只是让MySQL按索引顺序读,而不是随机跳转;SSD降低单次I/O延迟,但扫描100万行仍需百万级随机读(尤其回表时),吞吐瓶颈仍在逻辑层面。
容易被忽略的点:
- 复合排序(如
ORDER BY status, created_at)若只有created_at单列索引,会退化为Using filesort,磁盘临时文件风险陡增 -
SELECT *在深分页中放大危害:每多一个TEXT/BLOB字段,内存拷贝和网络传输压力就翻倍 -
innodb_buffer_pool_size不足时,热数据无法常驻,导致原本可缓存的索引页也频繁换入换出
游标分页WHERE id > ?比OFFSET快的本质
它把“跳过N行”的线性操作,换成“范围扫描”的对数操作:WHERE id > 1234567 ORDER BY id LIMIT 20 直接定位到B+树中 id = 1234567 的位置,然后向右连续取20条——扫描行数恒定,与总数据量无关。
实操关键点:
- 排序字段必须严格单调、非空、有索引;自增
id最稳妥,created_at需补默认值防NULL - 多字段排序(如
ORDER BY status DESC, created_at DESC)必须用元组比较:WHERE (status, created_at) > (?, ?) - 前端必须传回上一页末尾的完整排序值,漏传任意一字段都可能导致漏数据或重复
延迟关联能救急,但不是万能解药
当业务强制要求支持任意页码跳转(如后台管理页的page=5000),SELECT t.* FROM t JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000, 20) tmp ON t.id = tmp.id 是止损最优选。
它把原SQL中“扫描1000020行+回表1000020次”,压缩成“扫描1000020行ID(覆盖索引)+回表20次”。但注意:
- 子查询仍要扫描
offset + size行,只是不回表;offset过大时依然慢,只是比原写法好 - 必须确保子查询中的
ORDER BY字段有覆盖索引,否则子查询本身就会Using filesort - 外层JOIN依赖主键等值匹配,不能用于
ORDER BY RAND()或无主键表
真正棘手的不是怎么写SQL,而是业务是否允许放弃“跳转任意页”——如果允许,游标分页几乎零成本;如果不行,延迟关联只是把性能悬崖往后推了一段,而非消除。**越晚意识到这点,后期重构代价越大。**











