索引下推(ICP)到底是什么?
你也许见过这样的 SQL 优化建议:"用 (name, age) 联合索引,查询 name LIKE '王%' AND age = 30 时,MySQL 5.6 之后的索引下推能减少回表。"
但你可能困惑:
- 为什么没有索引下推时,
age = 30不能直接在索引里判断? - 索引下推到底改变了什么?
一、准备一个例子
表 user:
| id | name | age |
|---|---|---|
| 1 | 王明 | 25 |
| 2 | 王红 | 30 |
| 3 | 王芳 | 28 |
| 4 | 李强 | 30 |
| 5 | 王伟 | 30 |
| 6 | 张丽 | 30 |
索引:(name, age) 联合索引。
查询:
sql
SELECT * FROM user
WHERE name LIKE '王%' -- 前缀匹配,可以用索引
AND age = 30;
目标:找出所有姓"王"且年龄为 30 的用户。
二、没有索引下推(MySQL 5.6 之前)
存储引擎 (InnoDB)只做一件事:根据 name LIKE '王%' 找到所有满足该条件的记录位置(主键 id)。
它不关心 age = 30 这个条件 ------ 这是 Server 层的活儿。
流程如下:
-
存储引擎扫描联合索引,找到所有
name以"王"开头的记录。索引内容(顺序按 name,name 相同时按 age):
name age id 王明 25 1 王红 30 2 王芳 28 3 王伟 30 5 李强 30 4 张丽 30 6 "王%"匹配到前 4 行:id = 1,2,3,5。
-
存储引擎用这 4 个 id 依次回表(去主键索引取完整行数据),将完整行返回给 Server 层。
-
Server 层拿到 4 行数据后,再应用
age = 30过滤,只保留 id=2 和 id=5 的两行。
结果:回表 4 次,实际只需要 2 行。浪费了 2 次随机 I/O。
三、有索引下推(MySQL 5.6+)
索引下推(Index Condition Pushdown, ICP) 改变分工:Server 层把 age = 30 这个条件也"下推"给存储引擎,让存储引擎在扫描索引时提前判断。
流程:
-
存储引擎扫描联合索引,找到
name LIKE '王%'的记录。每读到一条索引记录(此时还没回表),立刻检查
age = 30是否成立。- 读 (王明,25) → age=25 不满足 → 跳过,不回表。
- 读 (王红,30) → 满足 → 记录 id=2,稍后回表。
- 读 (王芳,28) → 不满足 → 跳过。
- 读 (王伟,30) → 满足 → 记录 id=5。
-
只有满足
name LIKE '王%' AND age = 30的 id(2 和 5)才去回表取完整行。 -
Server 层直接拿到最终 2 行数据,无需再过滤(因为条件已经在索引里判断过了)。
结果:回表 2 次,减少了一半的随机 I/O。
四、你可能纠结的几个专业点
1. "为什么无 ICP 时,不能在索引里直接判断 age?"
因为老版本 MySQL 的存储引擎与 Server 层分工僵硬:
- 存储引擎只负责"根据索引查找条件找到记录位置并回表"。
- Server 层负责所有
WHERE条件的过滤。 - 存储引擎不认识
age = 30(它没有被"下推")。
ICP 打破了这堵墙:Server 把部分条件传给存储引擎,让它在索引页内部提前过滤。
2. "不是说 LIKE '王%' 之后非等值匹配会停止,用不到 age 索引吗?那 ICP 怎么还能用?"
这里有两个不同概念:
-
索引查找(ref / range) :
联合索引
(name, age)中,name LIKE '王%'只能用到 name 列来缩小扫描范围,age 列无法用于二分查找或范围跳跃,因为不同 name 下的 age 不是全局有序的。这就是"匹配停止"。 -
索引下推(ICP) :
虽然不能利用 age 的有序性来加速查找,但扫描过程中 ,每一行索引记录本来就包含 age 字段。ICP 只是朴素地逐行判断
age = 30,不需要有序。它是在扫描完 name 范围后、回表前做的额外过滤。
打个比方:
- 无 ICP 时:你拿着索引找到所有姓王的门牌号,挨个进屋(回表)问年龄。
- 有 ICP 时:你在门口(索引里)先透过猫眼看一眼年龄,不是 30 的直接不敲门。
3. "为什么 b > 10 会导致 c 无法用于索引查找?"(进阶但常见)
这是一个很容易混淆的点。我们用一个更通用的例子来说明:
假设有联合索引 (a, b, c),查询:
SELECT * FROM t WHERE a = 1 AND b > 10 AND c = 5
索引的物理排序规则:
- 先按
a排序 → 再按b排序 → 再按c排序。
数据示例(只显示索引列):
| a | b | c |
|---|---|---|
| 1 | 5 | 2 |
| 1 | 12 | 1 |
| 1 | 12 | 9 |
| 1 | 15 | 4 |
| 1 | 20 | 7 |
| 2 | 8 | 3 |
索引查找能做的是:
a=1:定位到第一个a=1的记录(1,5,2)。b>10:因为b在a=1范围内有序,可以跳过b<=10的记录,直接定位到第一个b>10的记录(1,12,1)。
扫描起点就是(1,12,1),然后沿着链表向后扫描,直到a不再是 1。
c=5 为什么不能用于查找?
因为 c 在 b>10 这个范围内是无序 的。在扫描区间内,c 的取值是:1, 9, 4, 7 ...... 没有规律,所以无法用二分或跳步定位到 c=5 的位置。
换句话说,遇到第一个范围查询(b>10),该列之后的索引列(c)都无法用于缩小扫描范围 ,只能作为过滤条件。
那 ICP 能做什么?
虽然 c 不能用于查找,但索引叶子节点中确实存了 c 的值。无 ICP 时,上面 4 行(b>10 的所有行)都会回表,然后 Server 发现 c=5 都不满足,白白回表。
有 ICP 时,存储引擎在扫描索引过程中,每读到一行 (a,b,c) 就立即判断 c=5,因为都不满足,所以一次回表都不做,直接返回空结果。
一句话区分:
- "无法用于查找" = 不能帮助快速跳过区间。
- "ICP 可用" = 可以在索引内逐行过滤,减少回表。
4. "等值条件如 name='王' AND age=30 需要 ICP 吗?"
不需要。因为 name='王' 是等值,联合索引可以同时用上 name 和 age 两个列 来精确定位,直接就能定位到 (王,30) 这一条记录,几乎没有多余的扫描。
ICP 主要解决索引查找只能用前缀列 (如 LIKE、<、> 等非等值)时,后续列无法参与查找,但可以通过 ICP 减少回表。
五、一张图总结 ICP 前后流程
text
【无 ICP】
索引扫描(name范围) → 得到 id 列表 [1,2,3,5] → 回表4次 → Server层过滤age=30 → 结果[2,5]
【有 ICP】
索引扫描(name范围) → 边扫描边判断age=30 → 得到 id [2,5] → 回表2次 → Server层直接返回
六、ICP 的适用场景(什么时候有效)
- 联合索引,且查询条件只有索引前缀列能用于查找 (如
LIKE 'prefix%'、col > value)。 - 其余列(后缀列)的过滤条件比较严格,能过滤掉大部分行。
- 回表代价高(比如表很大、非覆盖索引)。
不适用的情况:
- 覆盖索引(不需要回表,ICP 没有收益)。
- 分区表(部分版本限制)。
- 条件中涉及子查询或存储函数。
七、例子
假设索引 (a, b, c),查询:
SELECT * FROM t WHERE a = 1 AND b > 10 AND c = 5
- 索引查找能用到哪几列?
- ICP 可以帮上什么忙?
- 索引查找能用
a = 1和b > 10(因为b > 10是第一个范围条件,c无法用于查找)。 - ICP 可以在索引扫描过程中,对满足
a=1 AND b>10的每一行提前判断c=5,只对通过的行回表。如果没有任何行c=5,则回表次数降为 0。