MySQL深分页性能差的根本原因是OFFSET必须扫描并丢弃前N行,优化核心是绕过LIMIT offset,size模型;推荐游标分页(WHERE id>last_id)、延迟关联(子查询取ID再JOIN)或预生成映射表。

MySQL 8.0 的深分页(如 LIMIT 1000000, 10)不是“慢一点”的问题,而是根本性设计缺陷:它必须扫描并丢弃前 offset 行,无论有没有索引。优化的核心不是调参,而是绕过 LIMIT offset, size 这个执行模型本身。
延迟关联(Deferred Join)为什么在 MySQL 8.0 更有效
MySQL 8.0 的优化器对子查询物化(materialization)和半连接(semi-join)的支持更成熟,JOIN 子查询的执行计划更稳定,避免了早期版本中常见的临时表全扫描退化。
- 必须确保子查询中的
ORDER BY字段有覆盖索引,否则SELECT id FROM t ORDER BY create_time LIMIT 1000000, 10本身就会触发Using filesort - 主表
JOIN时,ON t.id = tmp.id必须依赖主键或唯一索引——InnoDB 会走聚簇索引查找,这是 O(log n) 的 B+ 树定位,不是随机 I/O - 如果业务允许只查部分字段,把
SELECT t.*改成SELECT t.id, t.name, t.status,配合覆盖索引,能进一步消除回表 - 注意:子查询结果集不能太大,
LIMIT 1000000, 10没问题,但LIMIT 1000000, 1000可能导致临时内存不足,建议单页 size ≤ 100
书签法(Bookmark Pagination)的实际落地要点
书签法本质是用 WHERE 条件替代 OFFSET,但它不是简单“记住上一页最后 ID”就完事,MySQL 8.0 下要特别注意边界、排序一致性与索引匹配。
- 必须使用单调递增且不重复的字段做书签,
id(自增主键)最稳妥;create_time需加microsecond或联合唯一约束,否则并发写入可能产生相同时间戳 - 排序方向必须与 WHERE 条件严格一致:
ORDER BY id ASC对应WHERE id > last_id;ORDER BY create_time DESC对应WHERE create_time - 复合排序场景(如
ORDER BY status, create_time DESC)需用元组比较:WHERE (status, create_time) ,MySQL 8.0 支持该语法,但索引必须是 <code>(status, create_time) - 前端传参必须校验:
last_id不能为空、不能为负、不能是非数字字符串,否则可能触发全表扫描
为什么不能混用延迟关联 + 书签法
有人试图写成 SELECT t.* FROM t JOIN (SELECT id FROM t WHERE id > ? ORDER BY id LIMIT 10) tmp ON t.id = tmp.id,这看似“双保险”,实则破坏了书签法的 O(1) 复杂度。
- 子查询里的
WHERE id > ?已经能精准定位起始位置,再套一层LIMIT 100000, 10是冗余的,反而让优化器放弃使用索引范围扫描,降级为全索引扫描 - EXPLAIN 会显示
type=range变成type=index,key_len从 8 变成 0,意味着没用上索引的有效长度 - 真实压测中,这种写法比纯书签法慢 3–5 倍,因为多了一次索引遍历和临时结果集构建
- 正确做法是二选一:高并发、不允许跳页 → 书签法;需支持跳页、数据无单调字段 → 延迟关联
真正难处理的不是第 100 万页怎么查得快,而是如何让前端不发 page=100000 这种请求——游标 token 和接口限流比任何 SQL 优化都管用。MySQL 8.0 再强,也救不了被滥用的分页语义。



















