做 MySQL 面试复盘时,经常被候选人用一句话搪塞过去:Using index condition 就是索引下推,Using where 就是回表。这个回答在大多数场景下不算错,但只要面试官追问一句「那 Extra 里同时出现 Using index condition; Using where 该怎么解释」,八成的人就卡住了。本文从优化器实际行为出发,把这两个标记的触发条件、组合关系、以及容易误判的边界场景逐个拆开,配合可直接执行的 SQL 与真实执行计划输出,方便面试前自测,也方便线上排查慢查询时对照。
先把两个标记的语义钉死
Extra 列是优化器对「这条 SQL 在执行阶段还额外做了什么」的注解,它不是互斥的枚举,可以有多个值并列出现。两句话概括:
- Using index condition:表示这条 SQL 用到了 Index Condition Pushdown(ICP,索引条件下推)。存储引擎层在遍历二级索引时,就用索引里能拿到的列先做一次过滤,把不满足条件的记录直接丢弃,减少回表次数。它是 存储引擎层 的行为。
- Using where:表示 server 层在拿到存储引擎返回的行之后,还需要用 WHERE 子句里的条件再过滤一次。它是 server 层 的行为,与用不用索引无关——哪怕 type=ALL 全表扫描,只要 Server 层需要再判条件,也会出现 Using where。
关键点在于:两者的判断层级不同,因此可以同时存在。ICP 过滤掉的是「索引列上能判定的部分」,剩下的条件(比如非索引列、或者下推后仍需复核的条件)回到 server 层再判,于是 Extra 里就同时打出 Using index condition; Using where。
ICP 从 MySQL 5.6 引入,默认开启,开关是
optimizer_switch中的index_condition_pushdown。5.7 / 8.0 默认值均为 on,可以用SET optimizer_switch='index_condition_pushdown=off';在会话级关掉做对比实验。
场景一:索引前缀 + 索引列范围,出现 ICP 且没有 Using where
建一张典型表,模拟订单查询:
CREATE TABLE t_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
amount DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL,
remark VARCHAR(128) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_user_status (user_id, status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 造 10 万行数据便于观察
INSERT INTO t_order (user_id, status, amount, created_at)
SELECT FLOOR(RAND()*1000), FLOOR(RAND()*5), ROUND(RAND()*1000,2),
NOW() - INTERVAL FLOOR(RAND()*365) DAY
FROM information_schema.columns c1, information_schema.columns c2
LIMIT 100000;
ANALYZE TABLE t_order;
执行下面这条 SQL,索引 idx_user_status 的前两列都是等值,第三列 created_at 是范围:
EXPLAIN SELECT id, amount FROM t_order
WHERE user_id = 100 AND status = 1 AND created_at >= '2024-01-01';
典型输出(8.0.33 实测):
+----+-------------+---------+------------+------+------------------+------------------+---------+-------------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+------------------+------------------+---------+-------------+------+----------+-----------------------+
| 1 | SIMPLE | t_order | NULL | ref | idx_user_status | idx_user_status | 9 | const,const | 102 | 33.33 | Using index condition |
+----+-------------+---------+------------+------+------------------+------------------+---------+-------------+------+----------+-----------------------+
这里只有 Using index condition,没有 Using where。原因:created_at 是索引的第三列,虽然因为前面两列等值而能参与范围定位,但它无法用于 ref 精确匹配(key_len=9,即 8 字节 user_id + 1 字节 status,不含 created_at 的 5 字节),于是 created_at 的比较被下推到引擎层,在遍历索引项时逐条判断,不满足的直接跳过,满足的才回表取 amount。整个过程 server 层无需再过滤,所以没有 Using where。
如果关掉 ICP:
SET optimizer_switch='index_condition_pushdown=off';
EXPLAIN SELECT id, amount FROM t_order
WHERE user_id = 100 AND status = 1 AND created_at >= '2024-01-01';
Extra 会变成 Using where——引擎把所有 user_id=100 AND status=1 的索引项对应的行全部回表,server 层再按 created_at 过滤。这一对比就是面试里最想听到的实验方法。
场景二:同时出现 Using index condition; Using where 的构造
把条件里塞进一个索引覆盖不到的列,比如 amount:
SET optimizer_switch='index_condition_pushdown=on';
EXPLAIN SELECT id, amount FROM t_order
WHERE user_id = 100 AND status = 1
AND created_at >= '2024-01-01'
AND amount > 500;
+----+-------------+---------+------+-----------------+-----------------+---------+-------------+------+----------+------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------+-----------------+-----------------+---------+-------------+------+----------+------------------------------------+
| 1 | SIMPLE | t_order | ref | idx_user_status | idx_user_status | 9 | const,const | 102 | 33.33 | Using index condition; Using where |
+----+-------------+---------+------+-----------------+-----------------+--------+-------------+-------------+------+----------+------------------------------------+
拆解:
- user_id、status 用于索引定位(ref)。
- created_at 在索引内,下推给引擎层过滤 → Using index condition。
- amount 不在索引 idx_user_status 内,引擎层无从判断,必须回表取出整行后由 server 层比较 → Using where。
所以「同时出现」并不矛盾,它精确表达了「索引内条件已下推、索引外条件仍需 server 层复核」。面试时如果能把这条 SQL 和它的 Extra 对应起来讲,基本就过关了。
场景三:只有 Using where 的两种情况
3.1 全表扫描
EXPLAIN SELECT id, amount FROM t_order WHERE amount > 900;
amount 上无索引,type=ALL,Extra 为 Using where。此时没有任何索引可下推,server 层只能逐行判条件。这说明 Using where 与是否走索引无关,很多人误以为出现 Using where 就是索引失效,这是高频错误。
3.2 索引定位后用非索引列过滤
EXPLAIN SELECT id FROM t_order
WHERE user_id = 100 AND remark = 'abc';
能靠 user_id 走 ref,但 remark 不在索引里,Extra 是 Using where,没有 index condition,因为没有索引内可下推的条件(user_id 已经用于 ref 定位,不属于「下推过滤」的范畴)。
这里有个容易混淆的细节:用于 ref / range 定位的条件不会计入 Using index condition,ICP 只作用于「索引列上、但未用于定位的那部分过滤条件」。user_id = 100 用于定位,所以不产生 ICP 标记。
场景四:覆盖索引下三个标记的优先级
把查询改成覆盖索引:
EXPLAIN SELECT user_id, status, created_at FROM t_order
WHERE user_id = 100 AND status = 1 AND created_at >= '2024-01-01';
+----+-------------+---------+------+-----------------+-----------------+---------+-------------+------+----------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------+-----------------+-----------------+---------+-------------+------+----------+--------------------------+
| 1 | SIMPLE | t_order | ref | idx_user_status | idx_user_status | 9 | const,const | 102 | 33.33 | Using where; Using index |
+----+-------------+---------+------+-----------------+-----------------+--------+-------------+------+----------+--------------------------+
Extra 变成了 Using where; Using index,index condition 消失了。原因是当查询所需的列全在索引里、没有回表动作时,ICP 省不了任何回表成本,优化器会退化成在索引上做普通过滤,Extra 用 Using index(覆盖索引)+ Using where 表达。记住这条:Using index condition 的价值在于减少回表,一旦无需回表,它通常不再出现。这是面试区分「理解 ICP 本质」与「背概念」的分水岭。
顺带一提,如果 created_at 上建了独立索引而 user_id 是主键,查询 WHERE created_at >= '2024-01-01' AND user_id > 10,因为 user_id 即主键、二级索引里自带主键,也可能触发 ICP 并伴随 Using where,key_len 会反映下推的列。排查时以 EXPLAIN 的 key_len 和 filtered 为准,不要凭直觉。
场景五:被 ICP 排除的语句类型
不是所有 SQL 都能用 ICP。以下情况即使条件满足也不会出现 Using index condition:
- 使用了聚簇索引(主键)扫描:主键索引本身就是整行数据,没有回表概念,ICP 无意义。
- 子查询:条件涉及子查询时,下推受限。
- 存储函数:条件里调用存储函数,引擎层无法求值。
- 触发器相关的场景、以及部分 MyISAM 上的行为差异。
- 索引列上用了
LIKE '%xx'这类无法用索引定位、且无法在索引项上比较的条件——其实这类条件本身就无法下推为 ICP,只会在 server 层用 Using where 过滤。
另外注意 filtered 列的含义:它是优化器估算的「经过该表条件过滤后剩余行的百分比」。ICP 生效时,filtered 会把索引内下推条件也算进去,所以看到 filtered 很小但 rows 很大时,不代表扫描行数一定很多——引擎层可能已经在下推阶段砍掉了大部分。要判断真实开销,用 EXPLAIN ANALYZE(8.0.18+)看 actual rows,比 EXPLAIN 的估算靠谱得多。
EXPLAIN ANALYZE SELECT id, amount FROM t_order
WHERE user_id = 100 AND status = 1
AND created_at >= '2024-01-01'
AND amount > 500;
输出里的 actual time=... rows=... loops=... 能直观看到回表前后行数差。如果关掉 ICP 后 actual rows 明显变大,就说明下推确实在起作用。
排查清单:遇到 Extra 怎么读
| Extra 组合 | 典型含义 | 排查动作 |
|---|---|---|
| Using index condition | 索引内条件已下推,回表次数已被削减,server 层无需再过滤 | 看 key_len 是否包含下推列;对比关闭 ICP 后的实际扫描行数 |
| Using where | server 层还需要过滤;可能走索引也可能全表扫 | 先看 type,若为 ALL 则考虑加索引;若为 ref/range 则看过滤列能否纳入联合索引 |
| Using index condition; Using where | 索引内条件下推 + 索引外条件服务器层复核 | 把 Using where 对应的列加入索引做覆盖,或评估是否值得建联合索引 |
| Using where; Using index | 覆盖索引,无需回表,条件全在索引内判定 | 已是很优形态,关注索引选择性与扫描行数即可 |
| Using index; Using where | 同上,顺序差异仅与版本/条件顺序有关 | 毋须处理 |
| Using filesort / Using temporary | 排序或分组无法用索引 | 补充排序前缀匹配的索引,或改写 SQL |
一个常见报错帮助定位问题:如果你为了验证 ICP 去改 optimizer_switch,误写成不存在的开关名,会直接报错:
ERROR 1231 (42000): Variable 'optimizer_switch' can't be set to the value of 'index_condition_push=off'
正确写法是完整拼写 index_condition_pushdown=off。另外,SET optimizer_switch 前建议先 SELECT @@optimizer_switch\G 记录原值,测试完恢复,避免影响同会话的后续查询。生产环境务必只在会话级修改,不要动 global。
小结
把判断压缩成三句话:
- Using index condition = ICP 生效,条件在索引项上就被引擎层过滤,目的是少回表;它一定伴随回表(除非无需回表时退化)。
- Using where = server 层还要过滤,与走不走索引无关;出现它不等于索引失效。
- 两者可并存,含义是「索引内可判的条件已下推,索引外条件回到 server 层处理」,通常提示存在可优化为覆盖索引或联合索引的空间。
面试时如果能把「关闭 ICP 前后 Extra 的变化」以及「为什么覆盖索引下 ICP 消失」这两点讲清楚,比背定义有说服力得多。
FAQ
Q1:Extra 里出现 Using where,是不是说明索引没用上?
不是。type=ref 或 range 时同样可能出现 Using where,那只表示有索引无法判定的条件需要在 server 层复核。判断索引是否生效看 type、key、key_len 三列,不要只看 Extra。
Q2:Using index condition 一定比 Using where 快吗?
不一定。如果下推的条件选择性很差(几乎不能减少回表),ICP 带来的只是少量 CPU 开销,性能提升有限,但通常不会更慢。判断收益要看 EXPLAIN ANALYZE 的实际回表行数差。
Q3:为什么我建了联合索引,Extra 还是 Using where 而不是 Using index condition?
先检查用于范围/过滤的列是否在索引中且未被更前置的等值条件截断(最左前缀),再看 key_len 是否覆盖了该列。如果条件列是索引的前缀等值列,它只参与定位,不产生 ICP 标记;只有「索引内、但不用于精确定位」的过滤列才会触发。
Q4:MySQL 5.6 之前有 Using index condition 吗?
没有。ICP 是 5.6 引入的特性,5.5 及更早版本的执行计划不会出现该标记,老版本排查时不要以它的缺失作为「索引失效」的依据。
Q5:生产上能长期关闭 index_condition_pushdown 吗?
不建议。关闭后回表次数增加,对 I/O 敏感的业务影响明显。除非遇到特定版本的 ICP 相关 bug 并已验证规避方案,否则保持默认 on。临时验证用会话级 SET 即可。
Q6:主键查询会看到 Using index condition 吗?
一般不会。ICP 的设计目标是减少二级索引回表,聚簇索引扫描本身就是整行数据,没有可省的回表动作,因此不会触发该标记。