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 中部分条件可以直接利用当前扫描的索引记录判断

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

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

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

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

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

相关推荐
云边有个稻草人1 小时前
向量数据库不必单独建一座“烟囱”:谈谈金仓KES的融合数据库架构
后端
晚安code1 小时前
TransmittableThreadLocal 线程池上下文传递:捕获重放恢复
后端
JoyT1 小时前
Claude Code 多会话协作:会话间消息与并行开发
后端
IT_陈寒1 小时前
SpringBoot自动配置的坑,把我整不会了
前端·人工智能·后端
赵大仁1 小时前
Human-in-the-loop:前端确认流与后端幂等
前端·后端·ai·agent·人机协作
Rain的Java大神之路1 小时前
如何避免订单重复提交
java·redis·后端·面试·架构·rabbitmq·rocketmq
程序员爱钓鱼2 小时前
Rust 泛型 Generics详解:编写可复用且类型安全的代码
后端·面试·rust
小满zs2 小时前
Go语言第九章(错误处理)
后端·go
桦说编程2 小时前
深入理解 FutureTask 状态机——从契约到实现
java·后端·性能优化