Skip Scan 是 MySQL 8.0.13 引入的复合索引优化机制,仅当前导列基数低且查询跳过该列时才会启用,本质是按前导列各值分别扫描再合并结果,并非无条件跳过前导列。

什么是 Skip Scan,它真能跳过前导列吗?
MySQL 8.0.13 引入的 SKIP SCAN 是一种针对复合索引的优化扫描方式,但它**不是无条件跳过前导列**——只有当前导列值重复度高(即基数低)、且查询条件跳过该列、只用后续列时,优化器才可能启用它。
典型触发场景:对 (a, b) 索引执行 WHERE b = ?,而 a 列只有几个固定值(比如 status 只有 'active'/'inactive'),这时 MySQL 会把索引按 a 的每个值分别做一次范围扫描,再合并结果。
- 不满足“前导列基数低”条件时,
SKIP SCAN不会被选中,哪怕语法合法 -
EXPLAIN中出现type: index_skip_scan才算真正生效,别光看key用了哪个索引 - 它无法用于
ORDER BY或GROUP BY涉及跳过列的场景(比如ORDER BY b仍不能利用(a,b)的 skip scan 排序)
怎么判断你的查询是否走上了 Skip Scan?
不能只信 EXPLAIN 的 key 字段——它可能显示用了索引,但实际是全索引扫描(type: index)。关键看 type 和 Extra:
-
type: index_skip_scan是唯一明确信号 -
Extra字段出现Using index condition; Using where是辅助线索,但不充分 - 用
EXPLAIN FORMAT=JSON查skip_scan节点是否存在,更可靠 - 在慢查询日志里看到
Rows_examined明显低于全表行数,且key是复合索引,值得怀疑是否走了 skip scan
为什么加了索引却没触发 Skip Scan?常见卡点
Skip Scan 是优化器的“备选策略”,极易被其他因素压制。最常踩的坑不是不会写 SQL,而是没意识到这些隐性限制:
- 前导列基数太高:比如
user_id作为(user_id, created_at)的第一列,几乎不可能触发 skip scan - 查询含
OR、IS NULL、函数包裹(如WHERE YEAR(created_at) = 2023),直接禁用 skip scan - 优化器认为全表扫描或其它索引更快——特别是当表小、或统计信息不准时,
ANALYZE TABLE后可能“突然生效” - 使用了
FORCE INDEX会绕过 skip scan 决策逻辑 - MySQL 8.0.13–8.0.20 存在若干 skip scan 漏判 bug,升级到 8.0.21+ 更稳定
Skip Scan 的代价和替代方案
它省的是 I/O,不是 CPU:每次“跳跃”都要重新定位 B+ 树分支,如果前导列有 100 个不同值,就相当于做 100 次小范围索引扫描。性能未必碾压单列索引:
- 若只查
b,建单独的(b)索引通常更快、更确定 - skip scan 在覆盖索引场景下才有明显优势(
SELECT b FROM t WHERE b = ?),否则回表开销叠加跳跃成本可能得不偿失 - 分区表 + 局部索引有时比 skip scan 更可控,尤其当数据天然按前导列分片时
- 注意监控
Handler_skip_scan_count状态变量,确认它是否真被高频使用,而不是“纸上谈兵”
skip scan 的真实价值不在“多快”,而在“让旧复合索引在新查询模式下还能复用”——但前提是,你清楚它什么时候愿意跳,什么时候宁愿绕路。










