Skip to content

最左前缀原则与索引下推(ICP)

提出问题

联合索引(Composite Index)是 MySQL 优化的核心手段之一,但很多开发者在建了联合索引后,发现某些查询并没有走索引,或者 EXPLAIN 的 rows 预估依然很大。问题出在哪?

关键就在两个概念上:最左前缀原则(Leftmost Prefix Principle)决定了联合索引哪些列能用上,索引下推(Index Condition Pushdown, ICP)决定了 MySQL 5.6+ 能在索引层面提前过滤多少数据。面试官问联合索引,往往不是考你背定义,而是给你一个 (a, b, c) 索引和几个 WHERE 条件,让你判断哪些走索引、哪些不走、为什么。生产上,建索引时没想清楚这两个机制,线上就会出现"明明有索引,SQL 还是慢"的尴尬局面。

分析问题

最左前缀原则:联合索引的"握手规则"

联合索引 (a, b, c) 本质上是一个按 a → b → c 顺序排序的 B+ 树。MySQL 只能从最左列开始匹配,一旦跳过中间列,后续列就走不了索引。

看下面这张表:

sql
CREATE TABLE `user` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `a` INT NOT NULL,
  `b` INT NOT NULL,
  `c` INT NOT NULL,
  `d` VARCHAR(100) DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `idx_a_b_c` (`a`, `b`, `c`)
) ENGINE=InnoDB;

走索引的场景:

  • WHERE a = 1 AND b = 2 AND c = 3 → 三列全走,type 为 ref,key_len = 12(3 × INT 4 字节)
  • WHERE a = 1 AND b = 2 → 走 a、b 两列,type 为 ref
  • WHERE a = 1 → 走 a 列,type 为 ref
  • WHERE a = 1 ORDER BY b → a 用于过滤,b 用于排序,避免 filesort

不走索引或部分走索引的场景:

  • WHERE b = 2 AND c = 3完全不走(跳过了 a),type 为 ALL,全表扫描
  • WHERE a = 1 AND c = 3a 走索引,c 不走(跳过了 b),但 MySQL 5.6+ 会用 ICP 在索引层面过滤 c
  • WHERE a = 1 AND b > 2 AND c = 3a 和 b 走索引,c 不走(b 是范围查询,c 在 b 后面,范围后的列失效)

最后一个场景最容易被忽略。很多人以为 b > 2 之后 c = 3 还能走索引,实际上 B+ 树在 (a, b, c) 排序下,b 是范围后,c 的索引顺序就被破坏了。正确的做法是:把等值条件的列放在前面,范围查询的列放在后面。

索引下推(ICP):减少回表的"预过滤器"

MySQL 5.6 引入的 ICP,是存储引擎层的一个优化。在没有 ICP 的情况下,当通过 idx_a_b_c 回表时,MySQL 的行为是:

  1. 用索引的 a 列找到所有满足 a = 1 的记录
  2. 回表拿到完整行
  3. 在 Server 层再过滤 c = 3

有了 ICP 之后,存储引擎层在扫描索引时,直接判断 c = 3 是否满足——只回表满足过滤条件的行,减少了回表次数。

sql
-- 联合索引 (a, b, c)
EXPLAIN SELECT * FROM user WHERE a = 1 AND c = 3;

-- Extra 字段会显示: Using index condition

Extra 中看到 Using index condition 即说明 ICP 生效。注意区分三个常见的 Extra 值:

Extra 值含义是否回表
Using index覆盖索引,索引已包含所有查询字段
Using index conditionICP 生效,索引层过滤后仍需回表是(但减少了回表行数)
直接回表后过滤

面试时追问最多的问题就是区分 Using indexUsing index condition。前者是覆盖索引——根本不需要回表;后者只是减少了回表次数,但还是要回表。

ICP 的局限性

ICP 并不是万能的,有三个限制需要记住:

  1. 只适用于二级索引。聚簇索引的叶子节点就是数据行,不需要回表,所以没有 ICP 一说。
  2. 只下推索引列上的条件WHERE a = 1 AND d = 'xxx' 中,d 不在索引中,无法下推。
  3. 不减少索引扫描行数。以 WHERE a = 1 AND c = 3 为例,索引扫描的行数仍然是 a = 1 的全部记录,ICP 只是减少了回表的行数。减少索引扫描行数要靠缩小 a = 1 的范围或另建索引。

MySQL 8.0 对 ICP 做了增强,支持更多函数条件下推(如 DATE_FORMATSUBSTRING 等),但核心原理不变。

生产踩坑:一条 SQL 慢两天,ICP 兜不住

去年接手一个二手交易平台的订单查询接口,线上慢查告警天天报。表结构简化后如下:

sql
CREATE TABLE `order` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `seller_id` INT NOT NULL COMMENT '卖家ID',
  `buyer_id` INT NOT NULL COMMENT '买家ID',
  `status` TINYINT NOT NULL COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成 4已取消',
  `create_time` DATETIME NOT NULL,
  `amount` DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_seller_status_time` (`seller_id`, `status`, `create_time`)
) ENGINE=InnoDB;

数据量:约 800 万行。接口查的是"某个卖家最近 30 天的已完成订单":

sql
SELECT * FROM `order`
WHERE seller_id = 12345
  AND status = 3
  AND create_time >= '2025-06-01'
  AND create_time < '2025-07-01';

EXPLAIN 结果:type=refkey=idx_seller_status_timekey_len=8rows=12 万Extra=Using index condition

出问题的地方: key_len=8 说明只用了 seller_id(4字节)+ status(4字节)两列,create_time 没走到索引列上——因为 ICP 虽然能把 create_time 过滤掉,但索引扫描行数还是「seller_id=12345 AND status=3」的全部记录,约 12 万行。每行都要进索引扫描、ICP 判断、回表,接口平均耗时 2.3 秒,高峰期把数据库 CPU 打到 80%。

解决方案: 把索引顺序调整为 (seller_id, create_time, status),因为 create_time 是范围查询,但业务上 seller_id + create_time 的区分度远高于 seller_id + status。改完后 key_len=12,rows=3000,耗时降到 15ms。

教训: 不要迷信 ICP 能兜底。ICP 只减少回表行数,不减少索引扫描行数。当扫描范围本身就很大时,ICP 兜不住性能。正确的做法永远是让索引列本身能过滤掉大部分数据。

面试追问:如何推算 key_len 判断索引用到了哪几列?

面试官给你一个 (a, b, c) 索引和 SQL,EXPLAIN 显示 key_len=12,问走了哪几列?

推算逻辑:

列类型字节数备注
INT4所有 INT 族同
BIGINT8
VARCHAR(n) utf8mb4n×4 + 22 是变长字段长度前缀
CHAR(n) utf8mb4n×4定长,无长度前缀
NOT NULL不加额外字节
NULL加 1 字节额外 1 字节标记 NULL

回到 idx_a_b_c,三列都是 INT NOT NULL:

  • 走 a = 4 字节
  • 走 a+b = 8 字节
  • 走 a+b+c = 12 字节

所以 key_len=12 说明三列全走。key_len=8 说明只走了 a+b。面试时问这个,考的是你知不知道 EXPLAIN 能反推索引使用情况,结合起来判断最左前缀是否匹配完整。

总结

面试话术示例(2 分钟内能说清楚版):

联合索引 (a, b, c) 下,最左前缀原则要求从最左列开始匹配,跳过中间列会导致后续列走不了索引,范围查询后的列也会失效。MySQL 5.6+ 的 ICP 能在索引层提前过滤 WHERE 条件,减少回表行数,EXPLAIN 的 Extra 行看到 Using index condition 就是 ICP 生效。但 ICP 只减少回表次数,不减少索引扫描行数,所以建索引时还是要把等值条件放在前面、范围条件放在后面,必要时建覆盖索引。

生产避坑:

  • 建联合索引前,先分析业务查询的 WHERE 条件顺序,把最常作为等值条件的列放前面
  • 不要因为 ICP 能兜底就随意建索引——ICP 比真正走索引列的效率低,它只是兜底方案
  • EXPLAIN 确认 key_len 值可以推算索引用到了哪几列(INT=4 字节,BIGINT=8 字节,varchar(n) utf8mb4 ≈ n×4+2 字节)
  • 区分度优先于等值/范围顺序:等值条件区分度低 + 范围条件区分度高时,范围列放前面反而更好

参考:MySQL 官方文档 - Index Condition Pushdown Optimization;《高性能 MySQL》第 5 章索引优化

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。