以前学 MySQL 的时候,你一定背过一条"铁律":联合索引必须遵循最左前缀,只要查询条件漏掉最左边的列,索引就彻底报废。 但 MySQL 8.0 之后,这个铁律被悄悄打破了------索引跳跃扫描来了。今天我们就用最通俗的话,把它的原理、适用场景和致命限制彻底讲清楚。
一、先打破固有认知
老规矩是这么说的:
联合索引 (A, B, C),WHERE 条件里必须带上最左边的列 A,否则索引失效,直接全表扫描。
这个说法在 MySQL 5.7 及以前一点儿没错。 但从 MySQL 8.0.13 开始,引入了一项新优化:Index Skip Scan(索引跳跃扫描)。 它的核心就一句话:
哪怕 WHERE 里不写第一个列 A,只要第一个列的"不同值的数量"足够少,MySQL 照样能利用这个联合索引。
就像这篇文章开头写的,你的查询是:
SELECT * FROM tb_xx WHERE B = 'xxx' AND C = 'xxx';
没带 A?没关系,跳跃扫描帮你自动补上。
二、原理:到底怎么"跳"过去的?
联合索引 idx(A, B, C) 的物理结构是按 (A, B, C) 排序的。 没有 A 的查询在物理上就是散的,原本只能全表扫描。 但跳跃扫描的思路很巧妙,分三步走:
- 先把 A 的所有不重复值全找出来
比方说 A 列是"性别",只有 '男'、'女'、'未知' 这 3 种值。
- 自动把你的 SQL 拆成一堆"带 A"的小查询
MySQL 内部等价执行:
SELECT * FROM tb_xx WHERE A='男' AND B='xxx' AND C='xxx'
UNION ALL
SELECT * FROM tb_xx WHERE A='女' AND B='xxx' AND C='xxx'
UNION ALL
SELECT * FROM tb_xx WHERE A='未知' AND B='xxx' AND C='xxx'
- 每个小查询都完美满足最左前缀 A+B+C,精准走索引
每一次都是一个小范围扫描,扫完后就跳到下一个 A 值,所以叫"跳跃扫描"。
最后把这些结果汇总返回,你感觉就像直接用了索引一样。
👉 本质根本没"跳过"A,而是帮我们自动遍历 A 的所有值,强行补齐最左前缀。
三、亲手验证:执行计划里长什么样?
为了让你彻底相信,我们直接上表、上数据。
-- 建表:性别、城市、年龄,外加普通索引
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
gender VARCHAR(10),
city VARCHAR(20),
age INT,
INDEX idx_g_c_a (gender, city, age)
);
-- 塞一些数据,性别只有三种
INSERT INTO user (gender, city, age) VALUES
('男', '北京', 20),
('女', '北京', 20),
('未知', '上海', 25),
('男', '上海', 20),
('女', '北京', 25),
('未知', '北京', 20);
-- ... 可多复制几份让数据量上去
现在,我们故意不写 gender,直接查 city 和 age:
EXPLAIN SELECT * FROM user WHERE city = '北京' AND age = 20;
在 MySQL 8.0 里,你会看到类似这样的执行计划:
| id | select_type | table | type | key | Extra |
|---|---|---|---|---|---|
| 1 | SIMPLE | user | range | idx_g_c_a | Using index for skip scan |
关注两个关键点:
key 列用了 idx_g_c_a ------ 没写 gender,索引却用上了。
Extra 列显示 Using index for skip scan ------ 明确告诉你这是跳跃扫描。
如果是 MySQL 5.7,同样的 SQL 大概率是 type: ALL 全表扫描。
这就是跳跃扫描带来的质变。
四、为什么不是全表扫描,反而更快?
全表扫描是把整张表从头撸到尾,数据多的时候磁盘 I/O 爆炸。
跳跃扫描虽然也有多次"小查询",但每次都是精准的索引范围扫描,只读一小块 B+Tree 叶子节点。 当第一个列 A 重复值很多、不同值很少时(比如状态、性别、类型字段),拆出来的小查询只有十几个甚至几个,总成本远低于扫全表。
例如:
- 表有 100 万行,status 只有 5 种值(
'待支付','已支付','已发货','已完成','已取消')。 - 查询条件是
WHERE order_date = '2025-01-01',没写 status。 - 跳跃扫描只需顺序查 5 次索引,每次只扫描对应状态的
order_date范围,性能碾压全表扫描。
因此,跳跃扫描的命门就在于:前导列的区分度必须够低。
五、致命限制:什么时候会被打回原形?
并不是所有没写最左列的查询都能享受跳跃扫描,以下几种情况会直接让它"废掉":
- 前导列区分度太高
如果 A 列是订单号、手机号、UUID 这种几乎全表唯一的字段,不同值数量可能几十万、上百万。
MySQL 如果拆出上百万条小查询,花销比全表扫描还可怕,优化器会直接放弃跳跃扫描,回归全表扫描。 - 查询含有 GROUP BY 或 DISTINCT
官方明确说明:使用了GROUP BY或DISTINCT的查询,不会触发 Index Skip Scan。
因为跳跃扫描本质上是一种"多个有序结果合并再处理"的方式,和去重/分组逻辑冲突。 - 与其他优化有冲突
例如某些复杂的子查询、窗口函数,或者当优化器认为索引合并(index merge)更划算时,也可能不走跳跃扫描。 - 不代表所有 JOIN 都绝对安全
多表关联查询中,跳跃扫描可以作用在单个表的访问路径上,但如果 SQL 中有GROUP BY、DISTINCT或整体成本估算认为不合适,仍然不会使用。 - MySQL 版本 < 8.0.13 彻底没门
这是硬条件,低版本只能老老实实遵守最左前缀。
六、能手动控制吗?开关在这里
跳跃扫描默认开启,如果需要调试或关掉,可以执行:
-- 关闭索引跳跃扫描(整个会话)
SET @@optimizer_switch = 'skip_scan=off';
-- 开启索引跳跃扫描
SET @@optimizer_switch = 'skip_scan=on';
如果你在执行计划里总看不到 Using index for skip scan,可以先检查一下 optimizer_switch 里 skip_scan 是否为 on。
七、总结:一句话吃透跳跃扫描
- 以前(5.7):少了最左列,联合索引直接报废,只能全表扫描。
- 现在(8.0+) :只要联合索引第一列不同值很少 ,就算你漏掉它,MySQL 也会自动穷举它的所有值,生成多条"补齐最左前缀"的索引小查询,再合并结果------这就是索引跳跃扫描。
跳跃扫描的存在,就是给那种"首列是低基数状态字段、后列才是高频查询条件"的场景量身定做的性能救星。
合理设计联合索引时,可以适当把"低区分度"字段放在最左边,然后用跳跃扫描优雅地加速那些只查后面字段的 SQL,让你既省了复合索引的数量,又保住了查询速度。