一、联合索引:不只是多个字段的简单组合
联合索引,也叫复合索引,说白了就是把多个字段揉在一起建一个索引。很多新手觉得这玩意儿就是把单列索引拼起来,其实完全不是那么回事。
我记得刚入行那会儿,有个同事在订单表上建了三个单列索引:user_id、status、create_time。结果查询还是慢得要命。我一看执行计划,好家伙,MySQL 只选了一个索引用,其他两个根本没用上。这就是典型的「索引各自为政」的问题。
1.1 联合索引的物理存储结构
联合索引在 B+ 树里是怎么存的?我画个图你就明白了。
看到没?联合索引的排序规则是:先按第一个字段排,第一个相同再按第二个排,以此类推。这就像查字典,先按拼音首字母,再按第二个字母...
二、最左前缀原则:为什么「跳列」就不行?
最左前缀原则,是联合索引的灵魂。我见过太多人在这上面栽跟头了。
2.1 原则的本质
假设我们有索引 (a, b, c),那么以下查询能用上索引:
WHERE a = 1✅ 用到了 aWHERE a = 1 AND b = 2✅ 用到了 a, bWHERE a = 1 AND b = 2 AND c = 3✅ 用到了 a, b, cWHERE b = 2❌ 用不上!因为跳过了 aWHERE a = 1 AND c = 3⚠️ 只用到了 a,c 用不上
为什么会这样?你想想看,B+ 树里数据是按 (a,b,c) 整体排序的。如果你只给 b 的值,树根本不知道从哪开始找——因为相同 b 值可能分散在不同 a 值下面。
核心结论:联合索引的匹配,必须从最左边开始连续匹配,不能跳过中间的列。
2.2 一个让我印象深刻的案例
我曾经帮一家电商公司优化订单查询。他们的表有 5000 万行,索引是 (order_id, user_id, status)。业务方经常跑这样的查询:
SELECT * FROM orders
WHERE user_id = 12345 AND status = 'paid';
结果呢?全表扫描,每次查询 3 秒多。为什么?因为跳过了最左边的 order_id,索引根本用不上。
解决方案很简单:把索引改成 (user_id, status, order_id),查询立马降到 5 毫秒。你看,有时候就是字段顺序的问题。
三、联合索引的建立策略
建联合索引,我总结了几个实战经验:
3.1 字段选择五原则
| 原则 | 说明 | 举例 |
|---|---|---|
| 1. 等值条件放最左 | 经常用 = 的字段放前面 |
WHERE user_id = 1 |
| 2. 区分度高的放前面 | 唯一值多的字段优先 | user_id 比 status 更适合放前面 |
| 3. 范围查询放后面 | >、<、BETWEEN 的字段放右边 |
WHERE a = 1 AND b > 10 |
| 4. 频繁查询的字段优先 | 业务中最常用的查询条件 | 按日活用户查询习惯来定 |
| 5. 避免冗余索引 | (a,b) 和 (a) 不要同时存在 | (a,b) 已经覆盖了 (a) 的功能 |
我的小技巧:建索引前,先把业务中所有的查询 SQL 收集起来,统计每个字段出现在 WHERE 中的频率。频率最高的等值条件字段,放在最左边。这招我在多个项目里验证过,效果很好。
3.2 实战:如何设计一个高效的联合索引
假设我们要为订单表设计索引,常见的查询有:
WHERE user_id = ? AND status = ?WHERE user_id = ? AND create_time > ?WHERE status = ? AND create_time BETWEEN ? AND ?
我的做法是:
-- 索引1:覆盖查询1和2
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);
-- 索引2:覆盖查询3(注意这里status放前面)
CREATE INDEX idx_status_time ON orders(status, create_time);
为什么这么设计?因为查询1和2都带了 user_id,所以把它放最左边。查询3没有 user_id,需要单独建索引。
四、索引下推:MySQL 5.6 后的性能利器
索引下推(Index Condition Pushdown,ICP)是 MySQL 5.6 引入的优化。说实话,这个特性刚出来的时候我没太在意,直到有一次优化慢查询才真正体会到它的威力。
4.1 没有索引下推时发生了什么
假设索引是 (name, age),查询是:
SELECT * FROM users
WHERE name LIKE '张%' AND age = 25;
没有 ICP 时,MySQL 的做法是:
- 用索引找到所有 name 以「张」开头的记录(可能 1000 条)
- 回表,把 1000 条完整记录都取出来
- 在 Server 层过滤 age = 25
这就浪费了!明明 age 也在索引里,为什么不直接在索引里过滤掉?
4.2 索引下推的优化原理
有了 ICP 后:
- 用索引找到 name 以「张」开头的记录
- 在存储引擎层,直接用索引里的 age 字段过滤,只保留 age = 25 的
- 只对过滤后的少量记录回表
说白了,就是把过滤操作「下推」到了存储引擎层,减少了回表次数。
-- 查看索引下推是否开启
SHOW VARIABLES LIKE 'optimizer_switch'\G
-- 输出中会包含 index_condition_pushdown=on
-- 通过执行计划确认是否用了ICP
EXPLAIN SELECT * FROM users
WHERE name LIKE '张%' AND age = 25;
-- Extra 列会显示:Using index condition
注意:索引下推只适用于二级索引。对于聚簇索引(主键索引),因为数据就在索引上,不需要回表,所以也用不上 ICP。
4.3 索引下推的最佳实践
我在项目中总结了几点:
- 尽量把查询条件涉及的字段都放进联合索引,这样 ICP 才能发挥作用
- LIKE 模糊查询配合 ICP 效果明显,特别是前缀匹配的情况
- 范围查询后的等值条件也能利用 ICP,比如
WHERE a > 10 AND b = 5
举个例子,我曾经优化过一个用户搜索功能:
-- 原索引
CREATE INDEX idx_name ON users(name);
-- 优化后(利用ICP)
CREATE INDEX idx_name_age_city ON users(name, age, city);
-- 查询
SELECT * FROM users
WHERE name LIKE '王%'
AND age BETWEEN 20 AND 30
AND city = '北京';
-- 优化后,age 和 city 的过滤在索引层完成,回表次数减少 90% 以上
五、总结一下
联合索引和索引下推,说白了就是两件事:
- 联合索引:把多个字段揉在一起排序,遵循最左前缀原则。设计时把等值条件、高区分度的字段放左边。
- 索引下推:把过滤操作从 Server 层下推到存储引擎层,减少回表。MySQL 5.6 后默认开启,但需要合理的索引设计才能发挥效果。
嗯,这些就是我这些年和索引打交道的一些心得。下次建索引前,不妨先想想:这个查询到底能不能用上索引?最左前缀原则有没有被满足?能不能利用索引下推减少回表?想清楚了再动手,能少走很多弯路。