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

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

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

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

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

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

相关推荐
金銀銅鐵3 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路4 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x4 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn6 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈7 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780517 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123457 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒7 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记7 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598788 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端