根本原因是MySQL必须扫描offset + size行再丢弃前offset行,无法跳转;优化方案包括游标分页(WHERE排序字段>上一页末值)和延迟关联(子查询查主键再JOIN)。

LIMIT offset, size 在 offset 很大时会明显变慢,根本原因不是“MySQL 懒”,而是它必须扫描 offset + size 行,再丢弃前 offset 行——哪怕你只想要 20 条结果,它也得把前 100 万行全捞出来、挨个判断、再扔掉。这不是 bug,是设计使然。
为什么 LIMIT 1000000, 20 会扫 1000020 行?
MySQL 不支持“跳转到第 N 行”的物理寻址。它走的是索引 B+ 树,从头开始遍历,每读一行就计数一次,直到累计读够 offset + size 行才停。这个过程包含:
- 如果
ORDER BY字段有索引,先按索引顺序取 ID(可能很快) - 但如果
SELECT *或非覆盖字段存在,就要对每个 ID 回表查完整行——100 万次随机 I/O - 若
WHERE条件用的是二级索引,还要先查索引得 ID,再回表,两轮开销叠加 - 即使加了索引,
OFFSET本身不减少扫描量,只影响返回行为
用游标分页替代 OFFSET:最实用的方案
适用于“上拉加载”“下一页”类场景,不支持跳转到任意页码,但性能稳定、无偏移量衰减。
- 核心逻辑:记住上一页最后一条记录的排序键值(如
id或create_time),下一页查询用WHERE过滤 - 示例(主键自增):
SELECT * FROM orders WHERE id > 100500 ORDER BY id LIMIT 20 - 注意点:排序字段必须唯一或组合唯一,否则可能漏/重数据;
create_time有重复时得补id:WHERE (create_time, id) > ('2026-08-10', 99999) - 首次请求不能带
WHERE,需单独查第一页;后续所有页都依赖上一页末尾值,不可跳页
延迟关联 + 覆盖索引:兼顾兼容性和性能
当业务强依赖 OFFSET(比如后台支持跳转页码),又无法改前端交互时,可用此法绕过回表瓶颈。
- 第一步:只查索引字段(如主键
id),利用排序索引快速定位目标 ID 区间:SELECT id FROM orders WHERE status = 1 ORDER BY id LIMIT 100000, 20 - 第二步:用这些 ID 关联原表取全量:
SELECT o.* FROM orders o INNER JOIN (上面子查询) tmp ON o.id = tmp.id - 关键前提:子查询中
ORDER BY字段必须有索引;若WHERE条件字段无索引,仍会全表扫描 - 性能提升来自:子查询只走索引树(快),外层 JOIN 是主键等值查找(快),彻底避开深度扫描+回表
容易被忽略的细节:索引是否真的生效?
很多优化失效,不是方案不对,而是索引没建对或被隐式破坏。
-
ORDER BY a, b查询,要建INDEX(a, b),而不是分开两个单列索引 -
WHERE a = ? ORDER BY b,索引应为INDEX(a, b),否则排序无法复用索引 -
WHERE a > ? ORDER BY a可用同一索引;但WHERE a > ? ORDER BY b通常无法避免文件排序 - 字符串字段用
LIKE 'xxx%'可走索引,LIKE '%xxx'则完全失效 -
status IN (1,2)后接ORDER BY created_at,除非(status, created_at)是联合索引,否则排序大概率走 filesort
真正卡住性能的,往往不是“要不要换方案”,而是“有没有确认索引是否覆盖了整个查询路径”。游标分页看似要改业务逻辑,但它把问题从“数据库怎么更快跳过一百万行”降维成“数据库怎么更快找到下二十行”,这才是可解的方向。



















