索引下推(Index Condition Pushdown,ICP)
MySQL 5.6 版本引入,InnoDB / MyISAM 都支持,默认开启。
概括:
在索引遍历过程中,把 WHERE 里能用到索引字段的条件,直接在索引层过滤,减少回表次数。
一、没有 ICP 的时候怎么做
假设联合索引:idx_name_age (name, age) 表结构:
sql
CREATE TABLE user(
id INT PRIMARY KEY,
name VARCHAR(20),
age INT,
phone VARCHAR(11),
INDEX idx_name_age(name, age)
);
查询 SQL:
sql
SELECT * FROM user WHERE name='张三' AND age>18;
无 ICP(5.6 之前)执行流程
联合索引叶子节点保存 (name, age, 主键id)
- 根据
name='张三'在索引树找到所有匹配name='张三'的索引记录; - 不判断 age 条件,拿着这些记录里的主键 id,全部回表,读取完整行数据;
- 回到服务层,再过滤
age>18; - 返回结果。
问题:很多name='张三'但是age<=18的数据,白白做了回表,IO 开销大。
开启 ICP 之后
- 根据
name='张三'在索引树找到匹配索引记录; - 在索引层直接判断索引字段条件
age>18; - 只有索引记录同时满足 name 和 age,才拿主键 id 去回表;不满足直接丢弃;
- 回表读取完整行,返回。
✅ 核心优化点:在索引页就过滤掉一部分数据,减少回表次数。
二、ICP 适用条件
- 只能用于二级索引,主键索引不生效(主键索引本身就存完整数据,不存在回表)
- WHERE 条件里,部分字段在索引中,部分不在索引中
- 条件是范围查询之后的索引列(联合索引最左匹配,范围后面的字段无法用于索引查找,但可以用 ICP 过滤)
- 不能是聚合函数、子查询;
SELECT只查索引字段(覆盖索引)时,不需要回表,ICP 就没意义。
❗ 关键区分:
- 索引查找(Index seek) :用来定位索引起始位置,只能用最左前缀,遇到范围
> < between就停止;- 索引下推 ICP :范围后面的索引字段,不能用来定位起点,但可以用来过滤。
三、经典场景示例
场景 1:联合索引 + 范围查询(ICP 最典型场景)
索引:idx_a_b_c(a, b, c) SQL:
sql
SELECT * FROM t WHERE a=10 AND b>20 AND c=30;
联合索引最左匹配规则: a=10 等值,b>20 范围,b 之后的 c 无法参与索引查找。
a=10:等值匹配 → 定位到所有 a=10 的这一段索引区间 ✅b>20:范围查询 在 a=10 这一组里,找到第一个 b>20 的位置,然后顺序向后扫描一段连续索引 。 ⚠️ 重点:经过b>20这个范围之后,这一段扫描出来的数据里,b 不再是一个固定值,b 是 21、22、23...... 不断变化。原本只有当 b 固定不变的时候,c 才是有序的;现在 b 五花八门,c 在这段范围里不再全局有序!
联合索引里,c 的有序性,仅建立在前面所有字段全部相等的前提下。
- a 固定,b 固定 → c 有序
- a 固定,b 是一个范围(多个不同 b)→ c 无序
索引想要快速定位
c=30,必须 c 是有序的,才能二分查找。 现在 c 乱序了,无法利用索引快速过滤 c,只能:通过索引找到
a=10 AND b>20的所有行,回表取出完整数据,再在内存里过滤 c=30对比演示,更好理解
索引
idx(a,b,c)
where a=10 and b=20 and c=30a 等值,b 等值,c 等值 → 三列全部走索引定位 ✅where a=10 and b>20 and c=30a 等值,b 范围 → b 之后 c无法索引定位,只能扫描后过滤 c ❌where a=10 and b=20 and c>30a 等值,b 等值,c 范围 → c 是第一个范围,c 后面无字段,没问题 ✅优化方案
如果业务经常
a=xx AND b>xx AND c=xx,调换索引顺序: 把等值条件放范围前面:
idx_a_c_b(a,c,b)条件:a=10 AND c=30 AND b>20
- a 等值,c 等值,b 范围 等值列全部放在范围列之前,就可以充分利用索引。
没有 ICP: 找到所有 a=10, b>20 的索引记录 → 全部回表 → 在 MySQL 服务层过滤 c=30。
开启 ICP: 找到 a=10, b>20 的索引记录 → 在索引层直接判断 c=30,不满足直接丢掉,只把满足 c=30 的记录回表。
这里
c在范围b>后面,不能用于索引定位,但是可以 ICP 过滤。
看执行计划 explain,Extra 字段会出现 Using index condition,代表 ICP 生效。
sql
Extra: Using index condition
区分:
Using index:覆盖索引,不需要回表;
Using index condition:索引下推 ICP; 两个可以同时出现。
场景 2:ICP 不生效的场景
① 条件字段不在索引里
索引 idx_a_b(a,b)
sql
SELECT * FROM t WHERE a=1 AND b>2 AND d=5; -- d不在索引
d=5 无法下推,只能回表后过滤;只有 a、b 可以在索引层处理。
② 覆盖索引,不需要回表
SELECT a,b,c FROM t WHERE a=10 AND b>20 AND c=30;
查询字段全部在索引里,不需要回表。此时 ICP 没有收益,Extra 不会出现 Using index condition。
③ 主键索引
主键索引叶子节点保存整行数据,本身没有回表操作,ICP 无效。
④ 子查询、函数操作
SELECT * FROM t WHERE a=10 AND b>20 AND DATE(c)='2026-01-01';
DATE(c) 函数,字段发生隐式转换,ICP 无法下推。
四、ICP 的局限性
- ICP 只是过滤,不能减少索引扫描行数(explain 里的 rows 是预估扫描索引行数,ICP 不会减少这个 rows,只会减少回表次数)
- ICP 只能过滤索引上存在的字段,非索引字段条件必须回表后过滤
- 对于 InnoDB,ICP 只支持二级索引;
- ICP 无法优化排序、分组。
五、开关控制
-- 查看是否开启ICP
show variables like 'optimizer_switch';
-- 关闭ICP(测试对比用)
set optimizer_switch = 'index_condition_pushdown=off';
-- 开启ICP
set optimizer_switch = 'index_condition_pushdown=on';
六、一句话总结面试版
索引下推 ICP 是 MySQL 5.6 引入的优化,针对二级索引。
当联合索引遇到范围查询后,后面的索引字段不能用来定位索引起点,但可以在索引页提前过滤数据,减少回表次数,降低随机 IO;
执行计划 Extra 显示Using index condition代表生效。ICP 不改变索引扫描行数,只减少回表。
七、面试常问对比
- ICP 和覆盖索引的区别?
ICP:减少回表次数;覆盖索引:完全不需要回表。二者可以同时存在。
- ICP 会不会减少 explain 里的 rows?
不会,rows 是预估索引扫描行数,ICP 只是扫描到索引记录时提前过滤,不减少索引扫描数量。