在mysql的日常优化工作中,联合索引(也叫复合索引)是出现频率非常高的一个知识点。单列索引只能加速一个字段的查询,而联合索引可以把多个字段组合成一个索引结构,不仅能减少索引数量,还能让范围查询、排序等操作获得更好的性能。但联合索引的使用有严格的前提条件,尤其是最左匹配原则,一旦违反,索引可能完全不起作用,查询会退化为全表扫描。本文从创建方法、底层原理、命中规则到失效场景,系统地讲清楚联合索引的正确用法。

一、联合索引的创建语法与基本操作
创建联合索引的语法和普通索引基本一致,区别在于括号中可以列出多个字段,字段之间用逗号分隔。mysql会按照字段声明的顺序构建B+树索引结构。基本语法如下:
-- 方式一:使用 create index 创建
create index idx_name_age on user(name, age);
-- 方式二:在建表时直接定义
create table user (
id bigint primary key,
name varchar(50) not null,
age int not null,
phone varchar(20),
key idx_name_age (name, age)
);
-- 方式三:使用 alter table 添加
alter table user add index idx_name_age(name, age);
需要注意几点:第一,联合索引的字段顺序非常关键,(name, age)和(age, name)是两个完全不同的索引,查询效果也截然不同。第二,mysql 5.7及之前版本单表索引数量建议不超过一定规模,联合索引可以替代多个单列索引,减少维护开销。第三,索引不是越多越好,每个索引都会占用存储空间,并且在增删改时需要同步维护,写入性能会受影响。
查看和删除联合索引的方式也很简单:
-- 查看表上的索引 show index from user; -- 删除索引 drop index idx_name_age on user; -- 或者 alter table user drop index idx_name_age;
二、最左匹配原则的原理与命中规则
理解最左匹配原则,需要先了解联合索引的存储结构。联合索引是一棵B+树,索引键是多个字段按顺序拼接而成的组合值。以(name, age)为例,数据先按name排序,name相同时再按age排序。这就像一本通讯录,先按姓氏排序,同姓的人再按名字排序——如果只知道名字而不知道姓氏,这本通讯录就帮不上忙了。
正因为这种排序方式,查询条件必须从索引的最左列开始连续匹配,索引才能被有效使用。具体规则如下:
-- 假设索引为 idx_name_age_phone(name, age, phone) -- 1. 命中索引:条件包含最左列 explain select * from user where name = '张三'; explain select * from user where name = '张三' and age = 20; explain select * from user where name = '张三' and age = 20 and phone = '138'; -- 2. 部分命中:中间列缺失,phone条件无法走索引 -- 只有name能用索引,phone需要回表后过滤 explain select * from user where name = '张三' and phone = '138'; -- 3. 索引失效:条件不含最左列,全表扫描 explain select * from user where age = 20 and phone = '138';
有一个容易混淆的点:where条件中字段的书写顺序不影响索引命中。where age = 20 and name = '张三'虽然age写在前面,但mysql的优化器会自动调整条件顺序,依然可以完整命中索引。真正决定能否走索引的是条件中是否包含了索引的最左前缀,而不是书写顺序。这一点是很多初学者容易误解的地方。
范围查询也需要特别注意。当最左列使用范围条件(如>、<、between、like 'xx%')时,该列之后的所有列都无法使用索引进行精确定位。例如where name > 'a' and age = 20中,只有name走了索引,age条件会在过滤阶段生效。因此在设计联合索引时,应尽量把等值查询的列放在范围查询列的前面。
三、索引失效的常见场景
除了不满足最左前缀,还有多种情况会导致联合索引失效,这些是生产环境慢查询的高发原因:
- 对索引列使用函数或运算:例如
where year(create_time) = 2024,对列施加函数后,索引中存储的原始值无法直接参与比较,只能全表扫描。应改写为范围条件:where create_time >= '2024-01-01' and create_time < '2025-01-01'。 - 隐式类型转换:phone是varchar类型,查询时写成
where phone = 13800000000,mysql会将phone列转换为数字再比较,等价于对列施加函数,索引失效。字符串列的查询值一定要加引号。 - 前导模糊匹配:
like '%abc'或like '%abc%'无法利用索引排序特性,只有like 'abc%'可以走索引。 - 使用or连接非索引列:如果or两边的列不都有索引,整个查询都无法使用索引。
- 使用不等于和not in:
!=、not in通常会导致优化器放弃索引,因为匹配的结果集可能占表的大部分。
排查索引是否命中,最可靠的工具是explain。执行计划中,type列显示为ref、range说明索引生效,如果是all则表示全表扫描;key列显示实际使用的索引名;key_len列可以判断联合索引命中了几个字段,是验证最左匹配效果的重要指标。
四、联合索引在排序与分页中的应用
联合索引的另一个重要价值是优化order by和limit分页。如果排序字段正好是索引的连续后缀列,mysql可以直接利用索引的有序性,避免额外的filesort排序操作。例如索引(name, age):
-- 可以利用索引直接按序读取,无需filesort
explain select * from user where name = '张三' order by age limit 10;
-- 无法利用索引排序,因为跳过了name,需要filesort
explain select * from user order by age limit 10;
-- 利用索引完成排序和分页,深分页场景性能提升明显
select * from user where name = '张三' order by age limit 100000, 10;
-- 进一步优化:先用覆盖索引定位主键,再回表,减少大量回表开销
select * from user u
join (
select id from user where name = '张三' order by age limit 100000, 10
) t on u.id = t.id;
深分页优化是联合索引的经典应用。直接limit大偏移量时,mysql需要扫描并丢弃前面的大量记录,每条记录都可能涉及回表。而先通过覆盖索引(只包含索引列的查询不需要回表)拿到目标主键,再精确关联取回完整数据,扫描量会大幅下降。
在设计联合索引时,可以遵循一个通用思路:把等值查询条件列放在最前面,其次是范围查询列,最后是排序列。这样既能让where条件充分使用索引,又能让order by借助索引有序性免于排序。索引设计完成后,务必结合真实的查询语句使用explain逐一验证命中情况,避免凭感觉建索引造成资源浪费。