MySQL 索引跳跃扫描(Index Skip Scan)

以前学 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 的查询在物理上就是散的,原本只能全表扫描。 但跳跃扫描的思路很巧妙,分三步走:

  1. 先把 A 的所有不重复值全找出来

比方说 A 列是"性别",只有 '男''女''未知' 这 3 种值。

  1. 自动把你的 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'
  1. 每个小查询都完美满足最左前缀 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,直接查 cityage

复制代码
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 范围,性能碾压全表扫描。

因此,跳跃扫描的命门就在于:前导列的区分度必须够低。

五、致命限制:什么时候会被打回原形?

并不是所有没写最左列的查询都能享受跳跃扫描,以下几种情况会直接让它"废掉":

  1. 前导列区分度太高
    如果 A 列是订单号、手机号、UUID 这种几乎全表唯一的字段,不同值数量可能几十万、上百万。
    MySQL 如果拆出上百万条小查询,花销比全表扫描还可怕,优化器会直接放弃跳跃扫描,回归全表扫描。
  2. 查询含有 GROUP BY 或 DISTINCT
    官方明确说明:使用了 GROUP BYDISTINCT 的查询,不会触发 Index Skip Scan。
    因为跳跃扫描本质上是一种"多个有序结果合并再处理"的方式,和去重/分组逻辑冲突。
  3. 与其他优化有冲突
    例如某些复杂的子查询、窗口函数,或者当优化器认为索引合并(index merge)更划算时,也可能不走跳跃扫描。
  4. 不代表所有 JOIN 都绝对安全
    多表关联查询中,跳跃扫描可以作用在单个表的访问路径上,但如果 SQL 中有 GROUP BYDISTINCT 或整体成本估算认为不合适,仍然不会使用。
  5. MySQL 版本 < 8.0.13 彻底没门
    这是硬条件,低版本只能老老实实遵守最左前缀。

六、能手动控制吗?开关在这里

跳跃扫描默认开启,如果需要调试或关掉,可以执行:

复制代码
-- 关闭索引跳跃扫描(整个会话)
SET @@optimizer_switch = 'skip_scan=off';

-- 开启索引跳跃扫描
SET @@optimizer_switch = 'skip_scan=on';

如果你在执行计划里总看不到 Using index for skip scan,可以先检查一下 optimizer_switchskip_scan 是否为 on

七、总结:一句话吃透跳跃扫描

  • 以前(5.7):少了最左列,联合索引直接报废,只能全表扫描。
  • 现在(8.0+) :只要联合索引第一列不同值很少 ,就算你漏掉它,MySQL 也会自动穷举它的所有值,生成多条"补齐最左前缀"的索引小查询,再合并结果------这就是索引跳跃扫描。

跳跃扫描的存在,就是给那种"首列是低基数状态字段、后列才是高频查询条件"的场景量身定做的性能救星。

合理设计联合索引时,可以适当把"低区分度"字段放在最左边,然后用跳跃扫描优雅地加速那些只查后面字段的 SQL,让你既省了复合索引的数量,又保住了查询速度。

相关推荐
jran-6 小时前
MySQL 用户与权限
数据库·mysql
無限進步D6 小时前
MySQL 排序与分页
数据库·mysql
唐青枫8 小时前
别只会写 IF:MySQL CASE WHEN 条件判断实战详解
sql·mysql
zhishijike9 小时前
全国行政区划sql(省市区)
数据库·sql·mysql
早川9199 小时前
Hbase、MySQL和Redis区别
redis·mysql·hbase
再战300年10 小时前
通过docker实现mysql一主多从
mysql·docker·容器
Irene199110 小时前
(课堂笔记)MySQL 基础(对比 Oracle 学习)
mysql·oracle
MY_TEUCK11 小时前
【2026最新版Linux安装Mysql】CentOS 7 安装 MySQL 8.4.9 完整流程(RPM 手动安装+避坑+面试)
linux·mysql·centos
川石课堂软件测试11 小时前
接口测试常见面试题及答案
python·网络协议·mysql·华为·单元测试·prometheus·harmonyos