mysql 8.0 的 index skip scan 能跳过前导列,但需满足高基数前导列、查询条件跳过该列且仅用后续列等严格前提,本质是按前导列每个非null值分段执行多次等值索引扫描。

MySQL 8.0 的 INDEX SKIP SCAN 是什么,它真能跳过前导列?
能,但有严格前提:只有当联合索引的最左列(前导列)具有**高基数且重复值少**,同时查询条件**跳过该列、只用后续列**时,INDEX SKIP SCAN 才会被优化器选中。它不是“任意跳过”,而是把前导列的每个非 NULL 值当作一个子范围,对每个值单独执行一次索引扫描——本质是多次等值扫描拼起来的伪“跳跃”。
典型触发场景:WHERE last_name = 'Smith' 查询一张有联合索引 (first_name, last_name) 的表。此时 first_name 是前导列,但没在 WHERE 中出现,传统 B+ 树无法高效定位,而 8.0+ 可能启用 SKIP SCAN。
- 必须是 InnoDB 表(MyISAM 不支持)
- 联合索引至少两列,且前导列不能是
NULL占主导(如全为NULL或大量重复值,优化器会放弃) - 查询条件中不能包含前导列的范围或模糊匹配(如
first_name > 'A'或first_name LIKE 'J%'),否则退化为全索引扫描 - 可通过
EXPLAIN FORMAT=TRADITIONAL查看Extra字段是否含Using index skip scan
为什么加了索引却没走 INDEX SKIP SCAN?
最常见的原因是优化器预估成本后认为它不如其他路径——比如前导列基数太低(如 gender 只有 'M'/'F'),导致要扫描 2 次索引,还不如直接走主键再回表;或者统计信息过期,让优化器误判。
- 用
ANALYZE TABLE table_name更新统计信息,尤其在大批量写入后 - 检查前导列实际唯一值数量:
SELECT COUNT(DISTINCT first_name) / COUNT(*) FROM users;—— 若比值低于 0.01,SKIP SCAN 很可能被忽略 - 强制提示不生效:
USE INDEX和FORCE INDEX对 SKIP SCAN 无效;只能通过optimizer_switch='skip_scan=on'全局开启(默认已开),或改写查询 - 注意隐式类型转换:如果
last_name是VARCHAR但传入数字,会导致索引失效,SKIP SCAN 自然也不会触发
INDEX SKIP SCAN 和覆盖索引、松散索引扫描的区别
三者都试图减少数据访问,但机制完全不同:
-
Covering index:所有 SELECT 列和 WHERE 条件列都在同一索引中,无需回表。SKIP SCAN 不要求覆盖,它只解决“跳过前导列”的定位问题,仍可能回表取其他字段 -
Loose index scan(用于GROUP BY):利用索引有序性跳过重复组,适用于聚合,不用于普通 WHERE 查询;而 SKIP SCAN 是针对 WHERE 的逐值扫描 - 性能上,SKIP SCAN 的 I/O 开销 ≈ 前导列唯一值个数 × 单次索引深度查找。若前导列有 1000 个不同值,就相当于做了 1000 次小范围索引查找,比全表扫快,但远不如一次精准的等值查找
实操建议:什么时候该主动建“适合 SKIP SCAN”的索引?
别为了 SKIP SCAN 而建索引。优先保证高频查询的前导列出现在 WHERE 中;只有当你明确存在一类查询总是过滤非前导列、且该列基数较高、又不便调整查询逻辑时,才考虑把高基数列前置。
- 例如日志表常查
status,但status在联合索引(tenant_id, status, created_at)中是第二列——若tenant_id基数极高(百万租户)、且多数查询不带tenant_id,那这个索引对status查询几乎无用;此时应建独立索引(status)或重排为(status, tenant_id) - 用
sys.schema_index_statistics视图观察索引实际使用频率和扫描行数,比凭空猜测更可靠 - SKIP SCAN 是兜底能力,不是设计目标。真正稳定的优化,永远来自查询与索引结构的对齐
真正容易被忽略的是:即使启用了 SKIP SCAN,它的执行计划里不会显示“跳过了多少行”,只显示总扫描行数——这行数是所有子扫描之和,可能远大于你直觉预期的“一次查找”。看执行计划时务必结合 rows 和 filtered 综合判断。











