MySQL 索引下推(ICP):为什么能少回几次表?

MySQL 面试里有个题,单独问的时候不算多,但一问就容易露馅:

索引下推(ICP)是什么?

很多人能背出"减少回表次数"这六个字,再往下就被问住了------为什么能减少?省的是哪一步 IO?它跟覆盖索引又是什么关系?

索引下推(Index Condition Pushdown,简称 ICP)是 MySQL 5.6 引入的查询优化。

当你用二级索引查数据时,MySQL 会把本该在 server 层做的条件判断,下推到存储引擎层,在数据遍历的那一刻就顺手筛掉,少回几次表。

先看清 MySQL 的三层架构

一条 SQL 从你敲下回车,到拿到结果,MySQL 内部是分三层跑的。

最上面是 Client 层,就是你接触到的那些东西:

JDBC 代码、Navicat、命令行、各种 SQL 客户端都算。

中间是 Server 层,负责 SQL 的解析、优化和执行。

这里可以重点关注几个核心组件:

连接器、解析器、优化器和执行器。

连接器管连接、验权限。

解析器负责词法分析和语法分析,把 SQL 解析成 MySQL 能理解的内部结构。

优化器负责选择执行计划,比如决定走哪个索引、采用什么访问方式。

执行器拿着计划去跟存储引擎打交道。

最底下是存储引擎层,InnoDB、MyISAM、Memory 都属于存储引擎,负责数据的实际存储和读写。

为什么特意提这个?

因为索引下推玩的就是"Server 层把活分给存储引擎层"------没有这层分工,根本谈不上"下推"两个字。

没有 ICP,回表是这样发生的

用二级索引查数据时,存储引擎会遍历那棵索引 B+ 树,把满足索引条件的记录逐条找出来。

每条二级索引记录里都带着它对应的主键值。

剩下的条件判断在哪儿做?

在 server 层。

但 server 层不会直接拿到过滤好的结果。

存储引擎得先根据这些主键值回到聚簇索引,把完整行捞出来交给 server 层,server 层才能去判断其余条件成不成立。

问题就出在这儿:

如果第一个字段匹配出了很多行,交上来的主键就很多,回表次数就跟着暴涨。

明明大部分行在后面的条件上就会被淘汰,可它们已经白白回表走了一趟。

一张 user 表,看清差距

拿一张 user 表举例,字段有 id、name、age、gender、address,id 是主键,建了一个 (age, name) 的联合索引。

sql 复制代码
CREATE TABLE user (
  id      INT PRIMARY KEY,
  name    VARCHAR(50),
  age     INT,
  gender  VARCHAR(10),
  address VARCHAR(200),
  INDEX idx_age_name (age, name)
);

现在跑这条查询:

sql 复制代码
SELECT * FROM user WHERE age > 10 AND name LIKE '%张%';

没有 ICP 的时候,执行分两步。

第一步,存储引擎用 age > 10 在二级索引上圈出一段扫描区间。

因为 age 是索引最左列、记录按 age 排好序,所以 age > 10 能直接定位到一段连续的范围,不必全表扫。

这段区间里假定有 1000 条索引记录。

接下来要查 name LIKE '%张%'。

name 是索引第二列,可 %张% 是前缀模糊匹配------前缀都不知道,自然没法靠索引的有序性继续往下缩范围,所以这 1000 条区间只能一条条扫过去。

但有个关键事实:

name 本身就写在每一条二级索引记录里(二级索引不只存 age 和主键,也存了 name)。

也就是说,扫到每条记录时,name 的值是现成能读到的。

可没有 ICP 时,存储引擎不会顺便拿它来过滤,而是老老实实把这 1000 条记录的主键逐个拿去聚簇索引回表、取回完整行,再交给 Server 层判断 name 是否成立。

于是出现了浪费:

这 1000 条里最终可能只有 10 条 name 带"张",剩下 990 条本该在索引层就被刷掉,却已经白白回表走了一趟。

性能开销主要就花在这些无效回表上。

有了 ICP 之后,第一步就变了。

存储引擎先根据 age > 10 确定二级索引的扫描范围。

在扫描 (age, name) 索引记录的过程中,由于 name 也保存在索引记录里,存储引擎可以直接判断 name LIKE '%张%'。

不满足条件的记录就地跳过,不再根据其中的主键去聚簇索引读取完整行;

只有同时满足条件的记录,才需要继续回表取 SELECT * 需要的其他字段。

这里也正是 ICP 和覆盖索引的分界:

如果查询改成只取索引里已有的字段,比如 SELECT age, name ...,那可能直接走覆盖索引就完成,连最后这次回表都不需要------当然,具体是否采用覆盖索引仍由优化器根据成本决定。

在这个示例里,ICP 大致可以避免 990 次无效回表。

用个比方再串一遍

还是这个场景,换个说法更好记。

你要从仓库里找出"尺寸大于 10、并且是红色"的零件。

仓库管理员手里的索引卡按尺寸排了序,每张卡上还记了这件零件的颜色和它在货架上的箱号------但卡上只有这三项,零件本身仍在货架上。

没有下推时:

管理员把所有尺寸 > 10 的零件整箱从货架搬到你办公室,你再一个个开箱看颜色,红色的留下、其他的扔掉。

1000 个候选里其实只有 10 个是红的------也就是说有 990 个箱子白搬了。

有下推时:

你提前把"红色"也写进取货单。

管理员翻索引卡时,颜色就写在卡上、不用开箱就能看到,于是他先把非红色的剔掉,只把 10 个红零件的箱子从货架搬出来。

那 990 个非红的,连箱子都没开、更没搬。

少搬出来的那 990 个零件,对应的就是 ICP 省掉的 990 次无效回表。

这里要和覆盖索引分清:

覆盖索引相当于索引卡上把查询要的所有信息都写全了,管理员连货架都不用去。

ICP 不一样------卡上只有部分字段,所以最后那 10 个红零件还是得从货架搬出来(回表),只是不用再为被淘汰的 990 个白跑一趟。

减少回表,不等于不回表

这里有个特别容易混的点,必须掰清楚。

ICP 减少了回表次数,但回表这一步并没有被彻底消灭。

上面那条查询是 SELECT *,要的是整行,存储引擎层筛完之后,照样得去聚簇索引把完整行捞出来。

它省掉的是"根据二级索引中的主键去聚簇索引读取完整行"这一步里、最终会被 WHERE 条件淘汰的那部分读取------也就是不必要的回表读取。

覆盖索引才是真正的不回表------要查的字段刚好全在索引里,连聚簇索引都不用去。

它俩经常被一起考,记住一句话就够了:

覆盖索引是"不用去",索引下推是"少去几次"。

维度 覆盖索引 索引下推 ICP
核心作用 利用索引直接返回查询结果 在扫描索引时提前过滤记录
是否回表 查询被索引完全覆盖时不需要回表 如果还需要读取非索引字段,仍然可能回表
优化重点 避免回表 减少不必要的回表
条件 SELECT 所需字段都能从索引获得 WHERE 中部分条件可以直接利用当前扫描的索引记录判断

索引下推说到底就一件事:

在使用索引扫描数据时,原本需要拿到完整记录后再判断的一部分条件,如果所需字段本身就在当前扫描的索引里,就可以提前下推到存储引擎层进行过滤。

这样,不满足条件的记录会在索引扫描阶段被淘汰,不再触发后续的回表读取。

它减少的是不必要的回表次数,不是彻底消灭回表;

而覆盖索引解决的是查询结果完全可以从索引中获得,从而根本不需要回表。

相关推荐
蜗牛互联网5 小时前
WSL Containers GA:本地AI容器的生命周期、网络与治理验收
java·网络·人工智能·后端
Zhou1411366 小时前
SpringBoot_02_自动配置原理
java·spring boot·后端
子兮曰7 小时前
Node.js 50个优势场景盘点:一个人单干,为啥我多数时候只用它
前端·后端
IT_陈寒7 小时前
我花3小时debug,就因为JavaScript这个隐式转换坑
前端·人工智能·后端
程序猿阿越7 小时前
containerd如何拉取镜像
后端·kubernetes·源码阅读
一条小小yu8 小时前
为什么使用springboot
java·spring boot·后端
香瓜子rd8 小时前
MySQL InnoDB 并发控制核心原理:事务、隔离级别、MVCC 与锁机制
数据库·后端
RISCV_Explorer8 小时前
RISC-V RVV向量编程模型机制解析——vsetvl配置、掩码与尾元素策略及长度无关编程
后端·risc-v
对象存储与RustFS8 小时前
升级 RustFS 二进制不停机:一条一条换,留一条退路
后端·rust·开源
hasty8 小时前
限制写了却没生效:OpenTelemetry Go 的 Unicode 截断边界
开发语言·后端·golang