覆盖索引在深度分页中失效,是因为limit offset, size强制mysql扫描并丢弃大量行,即使字段全在索引中,优化器也可能因成本判断放弃覆盖索引,转为全索引扫描+using filesort。

覆盖索引在深度分页中失效,不是索引没建好,而是查询逻辑破坏了“只走索引、不回表”这个前提——LIMIT offset, size本身就会迫使 MySQL 读取并丢弃大量行,哪怕字段全在索引里,优化器也可能因成本判断放弃它。
为什么LIMIT 100000, 20会让覆盖索引“形同虚设”
MySQL 5.7 及更早版本对 LIMIT 的实现是硬性扫描:必须读满 offset + size 行,再丢弃前 offset 行。即使你建了 INDEX(status, created_at, id),只要子查询里写的是 SELECT * 或外层 JOIN 没约束驱动顺序,优化器就可能认为“回表 10 万次比走索引还省”,直接退化为全索引扫描 + Using filesort。
-
EXPLAIN中看到rows值远大于size,且Extra含Using filesort或Using temporary,基本就是覆盖索引被绕过了 - MySQL 5.7 不支持降序索引,
ORDER BY created_at DESC却只建了INDEX(created_at)→ 索引无法用于排序,必然触发文件排序 - WHERE 条件用了
status IN (1,2),那么created_at就不能作为索引的“连续后缀”参与范围扫描,整个联合索引的排序能力报废
SELECT id FROM ... LIMIT offset, size 子查询为何常不生效
这个写法本意是让第一层只查主键,但实际落地时极易翻车:
- 没加
STRAIGHT_JOIN:优化器可能把子查询物化成临时表,再对外层表嵌套循环,彻底失去索引优势 - 子查询里写了
SELECT *或非索引字段(比如SELECT user_id而索引是(status, id))→ 破坏覆盖前提,强制回表 - 索引缺失 WHERE 字段:例如只建了
INDEX(created_at, id),却查WHERE user_id = 100 ORDER BY created_at DESC→ 索引无法过滤,退化为全索引扫描 - MySQL 5.6 及以下版本对这种写法优化不足,需手动检查执行计划是否真走了
Using index
真正能稳住覆盖索引的写法:延迟关联 + 强制路径
核心是切断优化器的“自由发挥空间”,逼它按你设计的路径走:
- 联合索引必须严格匹配查询:过滤字段在前、排序字段居中、主键放最后,例如
INDEX(user_id, status, created_at, id)对应WHERE user_id = ? AND status = ? ORDER BY created_at DESC - 子查询只选
id,且明确用STRAIGHT_JOIN控制连接顺序:SELECT t1.* FROM orders t1 STRAIGHT_JOIN (SELECT id FROM orders WHERE user_id = 100 AND status = 1 ORDER BY created_at DESC LIMIT 100000, 20) t2 ON t1.id = t2.id;
- 如果
created_at允许 NULL,必须在 WHERE 中显式排除:AND created_at IS NOT NULL,否则 MySQL 可能放弃该索引排序 - 避免在 ORDER BY 中混用方向:
ORDER BY created_at DESC, id ASC会破坏索引范围扫描能力
比覆盖索引更可靠的选择:游标分页的硬性条件
当偏移量持续增长、且业务接受线性翻页时,游标分页几乎不会失效,但它对数据结构极其敏感:
- 排序字段必须单调唯一:首选自增
id;若用时间字段,必须补二级排序,如ORDER BY created_at DESC, id DESC,对应条件写成WHERE (created_at, id) - WHERE 条件必须和 ORDER BY 共用同一联合索引,且索引顺序与查询完全一致,例如
INDEX(user_id, created_at, id)支持WHERE user_id = 100 AND (created_at, id) - 不能跳页:用户输入页码时,得先用覆盖索引查出目标页首条记录的
id(例如SELECT id FROM orders WHERE user_id = 100 ORDER BY id LIMIT 100000, 1),再二次查询,这步仍慢,但只查id成本可控
最容易被忽略的是 NULL 值和索引顺序错位——哪怕索引建了四个字段,只要 WHERE 条件漏掉最左一列,或 ORDER BY 方向和索引定义不一致,覆盖就立刻失效。











