mysql深分页慢的根本原因是必须扫描并丢弃前offset行,即使只取10条;优化方案包括延迟关联(子查询先取id再join)和游标分页(where id > ? order by id limit ?)。

LIMIT 分页慢,不是因为 SQL 写得不对,而是 MySQL 真的会扫描并丢弃前 offset 行——哪怕你只想要 10 条。
为什么 LIMIT 1000000, 20 要扫 1000020 行?
MySQL 不支持“跳到第 N 行再取”,执行器必须从排序结果开头逐行计数,直到凑够 offset + size 行,再扔掉前 offset 行。这个过程无法跳过,也不能靠索引直接定位。
- EXPLAIN 中 rows 值极大,且 Extra 出现 Using filesort 或 Using temporary,基本就是这问题
- 即使 ORDER BY id 有主键索引,只要 SELECT * 涉及大字段或非索引列,仍要频繁回表,放大随机 I/O
- 偏移量每增加 10 万,耗时几乎线性增长,不是对数级优化空间
为什么加了索引还是慢?
索引能加速排序,但不能绕过“先取后弃”逻辑。 - 如果WHERE 条件没走索引,或者 ORDER BY 字段和 WHERE 字段没组成覆盖索引,MySQL 仍要回表或走文件排序
- 复合排序(如 ORDER BY created_at DESC, id DESC)要求索引字段顺序、方向完全匹配,否则索引失效
- SELECT * 导致大量无用字段加载,网络传输、内存拷贝、磁盘读取全被拖慢
延迟关联(Deferred Join)怎么写才有效?
核心是把“查 ID”和“查全量”拆开,让第一步只走轻量索引。 - 子查询必须只SELECT id,且 ORDER BY 字段要有索引(最好是主键或联合索引最左前缀)
- WHERE 条件必须下推到子查询里,否则外层 JOIN 会放大结果集
- 外层必须用 JOIN,不能用 IN (subquery) —— MySQL 8.0.22 之前不支持带 LIMIT 的 IN 子查询
- 示例:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY id DESC LIMIT 1000000, 20 ) AS tmp ON o.id = tmp.id;
游标分页为什么比 LIMIT 更稳?
它彻底放弃 OFFSET,用上一页末尾的排序值做条件,B+ 树只需往后遍历 size 条。
- 前提是排序字段单调唯一(如自增 id 或带唯一约束的 created_at),否则需加二级排序字段防重复
- 客户端必须保存上一页最后一条的 id 或时间戳,不能靠页码计算
- 方向必须一致:ORDER BY id ASC 对应 WHERE id > ?,混用 ASC/DESC 会出错
- 示例:
SELECT * FROM orders WHERE id > 1234567 ORDER BY id LIMIT 20;
真正卡住性能的,从来不是语法本身,而是你默认它能“跳过”数据——而 MySQL 从不跳。











