执行计划里 Using index condition 与 Using where 的区分场景

做 MySQL 面试复盘时,经常被候选人用一句话搪塞过去:Using index condition 就是索引下推,Using where 就是回表。这个回答在大多数场景下不算错,但只要面试官追问一句「那 Extra 里同时出现 Using index condition; Using where 该怎么解释」,八成的人就卡住了。本文从优化器实际行为出发,把

做 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 |
+----+-------------+---------+------+-----------------+-----------------+--------+-------------+-------------+------+----------+------------------------------------+

拆解:

  1. user_id、status 用于索引定位(ref)。
  2. created_at 在索引内,下推给引擎层过滤 → Using index condition。
  3. 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。

小结

把判断压缩成三句话:

  1. Using index condition = ICP 生效,条件在索引项上就被引擎层过滤,目的是少回表;它一定伴随回表(除非无需回表时退化)。
  2. Using where = server 层还要过滤,与走不走索引无关;出现它不等于索引失效。
  3. 两者可并存,含义是「索引内可判的条件已下推,索引外条件回到 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 的设计目标是减少二级索引回表,聚簇索引扫描本身就是整行数据,没有可省的回表动作,因此不会触发该标记。