最左前缀原则与索引下推(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 只能从最左列开始匹配,一旦跳过中间列,后续列就走不了索引。
看下面这张表:
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 为refWHERE a = 1→ 走 a 列,type 为refWHERE a = 1 ORDER BY b→ a 用于过滤,b 用于排序,避免 filesort
不走索引或部分走索引的场景:
WHERE b = 2 AND c = 3→ 完全不走(跳过了 a),type 为ALL,全表扫描WHERE a = 1 AND c = 3→ a 走索引,c 不走(跳过了 b),但 MySQL 5.6+ 会用 ICP 在索引层面过滤 cWHERE a = 1 AND b > 2 AND c = 3→ a 和 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 的行为是:
- 用索引的 a 列找到所有满足
a = 1的记录 - 回表拿到完整行
- 在 Server 层再过滤
c = 3
有了 ICP 之后,存储引擎层在扫描索引时,直接判断 c = 3 是否满足——只回表满足过滤条件的行,减少了回表次数。
-- 联合索引 (a, b, c)
EXPLAIN SELECT * FROM user WHERE a = 1 AND c = 3;
-- Extra 字段会显示: Using index conditionExtra 中看到 Using index condition 即说明 ICP 生效。注意区分三个常见的 Extra 值:
| Extra 值 | 含义 | 是否回表 |
|---|---|---|
Using index | 覆盖索引,索引已包含所有查询字段 | 否 |
Using index condition | ICP 生效,索引层过滤后仍需回表 | 是(但减少了回表行数) |
| 无 | 直接回表后过滤 | 是 |
面试时追问最多的问题就是区分 Using index 和 Using index condition。前者是覆盖索引——根本不需要回表;后者只是减少了回表次数,但还是要回表。
ICP 的局限性
ICP 并不是万能的,有三个限制需要记住:
- 只适用于二级索引。聚簇索引的叶子节点就是数据行,不需要回表,所以没有 ICP 一说。
- 只下推索引列上的条件。
WHERE a = 1 AND d = 'xxx'中,d 不在索引中,无法下推。 - 不减少索引扫描行数。以
WHERE a = 1 AND c = 3为例,索引扫描的行数仍然是a = 1的全部记录,ICP 只是减少了回表的行数。减少索引扫描行数要靠缩小a = 1的范围或另建索引。
MySQL 8.0 对 ICP 做了增强,支持更多函数条件下推(如 DATE_FORMAT、SUBSTRING 等),但核心原理不变。
生产踩坑:一条 SQL 慢两天,ICP 兜不住
去年接手一个二手交易平台的订单查询接口,线上慢查告警天天报。表结构简化后如下:
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 天的已完成订单":
SELECT * FROM `order`
WHERE seller_id = 12345
AND status = 3
AND create_time >= '2025-06-01'
AND create_time < '2025-07-01';EXPLAIN 结果:type=ref,key=idx_seller_status_time,key_len=8,rows=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,问走了哪几列?
推算逻辑:
| 列类型 | 字节数 | 备注 |
|---|---|---|
| INT | 4 | 所有 INT 族同 |
| BIGINT | 8 | |
| VARCHAR(n) utf8mb4 | n×4 + 2 | 2 是变长字段长度前缀 |
| CHAR(n) utf8mb4 | n×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 章索引优化