MySQL 联合索引设计与索引失效
一、业务场景
某电商系统的订单表 orders 已经积累了 2000 万条数据,运营后台的订单查询逐渐变慢,部分 SQL 执行时间超过 2 秒。
表结构包含以下字段:
| 字段 | 含义 |
|---|---|
id |
主键 |
user_id |
用户 ID |
status |
订单状态 |
create_time |
创建时间 |
pay_amount |
支付金额 |
原有索引:
sql
KEY idx_user_id (user_id),
KEY idx_status (status),
KEY idx_create_time (create_time)
业务中存在两类查询。
场景一:根据用户和订单状态查询
sql
SELECT id, status, create_time
FROM orders
WHERE user_id = 1001
AND status = 1
ORDER BY create_time DESC
LIMIT 20;
场景二:根据订单状态和创建时间查询
sql
SELECT id, status, create_time
FROM orders
WHERE status = 1
AND create_time >= '2026-10-01 00:00:00'
ORDER BY create_time DESC
LIMIT 20;
核心问题:
- 多个单列索引不一定能够高效满足多条件查询。
- MySQL 可能选择单个索引,也可能选择 Index Merge。
- 联合索引的字段顺序会影响索引定位效率。
- 即使存在索引,查询仍然可能出现扫描量大、额外排序等问题。
二、为什么多个单列索引不一定够用?
假设已经建立以下索引:
sql
KEY idx_user_id (user_id),
KEY idx_status (status),
KEY idx_create_time (create_time)
查询条件为:
sql
WHERE user_id = 1001
AND status = 1
MySQL 优化器会根据成本估算选择执行计划,可能选择某个单列索引,再过滤其他条件,也可能考虑 Index Merge(索引合并)。
1. 单索引访问
例如,MySQL 选择 idx_user_id:
- 根据
user_id = 1001找到候选记录。 - 读取记录并检查
status = 1。 - 根据
create_time排序。 - 返回前 20 条记录。
如果该用户的订单很多,就可能产生大量额外的数据读取和排序开销。
2. Index Merge
Index Merge 可以将多个索引的扫描结果进行合并,例如对多个索引结果进行交集或并集操作。
但它不意味着所有单列索引都会自动组合,也不保证一定比单索引访问更慢或更快。
对于高频、固定的业务查询,专门设计的联合索引通常更容易减少扫描和额外处理。
核心结论:不要只看表上有多少个索引,要看索引是否匹配真实 SQL 的查询条件与排序需求。
三、联合索引如何设计?
3.1 场景一:用户 + 状态 + 时间排序
SQL:
sql
SELECT id, status, create_time
FROM orders
WHERE user_id = 1001
AND status = 1
ORDER BY create_time DESC
LIMIT 20;
推荐评估的索引:
sql
CREATE INDEX idx_user_status_time
ON orders (user_id, status, create_time);
原理:
- 先根据
user_id定位索引范围。 - 再根据
status缩小范围。 - 最后利用
create_time的索引顺序满足排序需求。
前两个字段均为等值条件,因此可以利用前两个字段进行联合定位;第三个字段用于时间顺序扫描。
在满足排序方向和其他条件的情况下,MySQL 可以通过反向扫描满足 ORDER BY create_time DESC,不一定需要建立显式降序索引。
3.2 场景二:状态 + 时间范围 + 时间排序
SQL:
sql
SELECT id, status, create_time
FROM orders
WHERE status = 1
AND create_time >= '2026-10-01 00:00:00'
ORDER BY create_time DESC
LIMIT 20;
推荐评估的索引:
sql
CREATE INDEX idx_status_time
ON orders (status, create_time);
原理:
- 根据
status = 1定位状态对应的索引范围。 - 在该范围内根据
create_time进行范围扫描。 - 利用
create_time的索引顺序满足排序。 - 找到满足时间条件的记录后,返回所需结果。
这里需要注意:范围条件并不意味着索引中后续字段必然完全失效。它主要影响后续字段进一步参与索引定位的能力,其他优化能力仍需结合具体 SQL 和执行计划分析。
3.3 最终索引方案
如果两类查询都很常见,可以优先评估:
sql
CREATE INDEX idx_user_status_time
ON orders (user_id, status, create_time);
CREATE INDEX idx_status_time
ON orders (status, create_time);
| 查询场景 | 推荐索引 | 主要作用 |
|---|---|---|
| 用户 + 状态 + 时间倒序 | (user_id, status, create_time) |
联合过滤、辅助排序 |
| 状态 + 时间范围 + 时间倒序 | (status, create_time) |
状态过滤、时间范围扫描、辅助排序 |
注意:这不是要求无条件创建两个索引。应结合查询频率、数据分布、现有索引使用情况、写入压力和存储空间综合判断。
InnoDB 二级索引的叶子节点通常包含主键值,因此上述查询中的 id 一般不需要为了覆盖它而额外追加到联合索引末尾。
四、重点:联合索引的最左前缀原则
4.1 什么是最左前缀原则?
假设存在联合索引:
sql
CREATE INDEX idx_abc ON test_table (a, b, c);
InnoDB 会按照联合索引字段的顺序组织索引记录,其排序逻辑可以理解为:
- 首先按照
a排序。 - 当
a相同时,按照b排序。 - 当
a、b都相同时,再按照c排序。
因此,联合索引可以直接利用最左侧连续字段构成的前缀进行索引定位。
常见情况:
| 查询条件 | 是否可以利用该索引进行有效定位 | 原因 |
|---|---|---|
WHERE a = 1 |
可以 | 使用最左字段 a |
WHERE a = 1 AND b = 2 |
可以 | 使用连续前缀 a, b |
WHERE a = 1 AND b = 2 AND c = 3 |
可以 | 使用完整索引前缀 |
WHERE a = 1 AND c = 3 |
可以,但通常主要利用 a 定位 |
缺少中间字段 b |
WHERE b = 2 |
通常不能直接利用该索引高效定位 | 缺少最左字段 a |
WHERE c = 3 |
通常不能直接利用该索引高效定位 | 缺少最左字段 a 和 b |
WHERE b = 2 AND c = 3 |
通常不能直接利用该索引高效定位 | 缺少最左字段 a |
这里的"不能直接利用索引高效定位",不代表 MySQL 完全不会读取该索引。例如,某些情况下可能进行全索引扫描,或者利用索引覆盖减少回表;具体行为应以执行计划为准。
4.2 为什么 (a, b, c) 中,WHERE b = ? AND a = ? 也符合最左匹配原则?
这是一个非常重要的知识点。
假设索引为:
sql
CREATE INDEX idx_abc ON test_table (a, b, c);
查询为:
sql
SELECT *
FROM test_table
WHERE b = 20
AND a = 10;
这个查询可以利用 (a, b, c) 索引的 a、b 两个字段进行定位。
原因是:
最左前缀原则约束的是索引字段在索引结构中的顺序,并不约束 SQL 中 WHERE 条件的书写顺序。
下面两条 SQL 在逻辑上等价:
sql
WHERE b = 20 AND a = 10
sql
WHERE a = 10 AND b = 20
对于 AND 连接的等值条件,MySQL 优化器可以识别这些条件并选择合适的索引访问方式,不要求开发人员必须按照索引字段顺序编写 WHERE 条件。
可以这样理解:
- 索引结构的顺序是
(a, b, c)。 - 查询条件同时提供了
a和b的值。 - MySQL 可以先确定
a = 10的范围,再在其中定位b = 20。 - 因此,索引的连续前缀
(a, b)可以被利用。
对比:
sql
WHERE b = 20
由于没有提供 a 的条件,数据库无法像使用 (a, b) 前缀那样直接定位到 b = 20 对应的连续索引范围。
记忆要点:最左前缀原则看的是条件能否覆盖索引最左侧的连续字段,不是看 WHERE 条件的书写顺序。
4.3 为什么 (a, b, c) 中,WHERE a = ? AND c = ? 不能直接完整利用索引?
例如:
sql
SELECT *
FROM test_table
WHERE a = 10
AND c = 30;
虽然条件包含 a 和 c,但是中间缺少 b。
索引的顺序是:
text
a → b → c
当 a = 10 时,索引中可能包含多个不同的 b 值。由于没有指定 b,数据库不能直接把 c = 30 当作紧接在 a 后面的索引定位条件。
通常可以使用 a 定位,再检查 c 是否满足条件。部分版本和执行场景还可能利用 Index Condition Pushdown 等机制减少不必要的回表,但这不等于完整使用了 (a, b, c) 进行连续前缀定位。
五、联合索引字段顺序如何选择?
设计联合索引时,不能简单地认为"区分度最高的字段永远放第一位"。
应综合考虑以下因素:
- 查询模式:哪些字段经常一起出现在查询条件中?
- 等值与范围条件:通常优先安排等值过滤字段,再考虑范围字段。
- 排序需求 :索引顺序能否满足
ORDER BY,减少额外排序? - 最左前缀复用:该索引能否同时服务其他高频查询?
- 数据分布:字段的选择性、数据倾斜程度以及不同条件组合下的实际扫描量。
- 写入与维护成本:索引越多,插入、更新、删除时的维护成本通常越高。
例如:
sql
WHERE user_id = ?
AND status = ?
ORDER BY create_time DESC
可以考虑:
sql
(user_id, status, create_time)
而:
sql
WHERE status = ?
AND create_time >= ?
ORDER BY create_time DESC
可以考虑:
sql
(status, create_time)
这两个索引并非简单重复,因为两类查询的过滤条件不同。
六、常见索引失效或利用效率低的情况
6.1 对索引列使用函数
不推荐:
sql
WHERE YEAR(create_time) = 2026
更推荐:
sql
WHERE create_time >= '2026-01-01 00:00:00'
AND create_time < '2027-01-01 00:00:00'
普通 B-Tree 索引通常更容易对直接的列范围条件进行高效定位。
6.2 隐式类型转换
例如,某个字段是字符串类型,却使用不合适的数值类型进行比较:
sql
WHERE phone = 12345678901
如果 phone 是字符串字段,建议使用符合字段类型的参数:
sql
WHERE phone = '12345678901'
具体是否影响索引,要看 MySQL 的类型转换规则、字段类型和执行计划。
6.3 前导通配符
普通 B-Tree 索引通常无法有效支持以下查询的前缀定位:
sql
WHERE order_no LIKE '%123'
但下面这种前缀匹配通常可以利用索引:
sql
WHERE order_no LIKE 'ABC%'
6.4 联合索引缺少最左前缀
已有索引:
sql
(a, b, c)
查询:
sql
WHERE b = 10
通常无法直接利用该索引进行高效定位。
6.5 过滤条件和排序需求不匹配
即使查询使用了索引,如果索引顺序无法满足 ORDER BY,仍可能产生额外排序。
例如:
sql
INDEX (status, create_time)
适合评估:
sql
WHERE status = 1
ORDER BY create_time DESC
但对于多个状态值的 IN 查询、不同排序字段或不同排序方向,不能简单地认为该索引必然消除排序,需要检查执行计划。
6.6 优化器选择其他执行计划
即使索引存在,MySQL 也可能基于成本估算选择全表扫描或其他索引。
可能的原因包括:
- 查询需要读取大量数据。
- 索引选择性较低。
- 表规模较小,全表扫描成本更低。
- 统计信息或数据分布影响成本估算。
- 查询条件、排序和回表成本使其他执行计划更有优势。
因此,不应仅凭"有没有索引"判断 SQL 是否高效。
七、如何验证索引设计是否有效?
7.1 使用 EXPLAIN
sql
EXPLAIN
SELECT id, status, create_time
FROM orders
WHERE user_id = 1001
AND status = 1
ORDER BY create_time DESC
LIMIT 20;
重点观察:
| 字段 | 关注点 |
|---|---|
possible_keys |
优化器认为可能使用哪些索引 |
key |
实际选择了哪个索引 |
key_len |
使用的索引键长度,可辅助判断使用了哪些索引部分 |
type |
访问类型,需要结合具体场景判断 |
rows |
估算扫描行数 |
Extra |
是否出现 Using filesort、Using index condition 等信息 |
注意:
possible_keys不为空,不代表实际使用了索引。key_len不能脱离字段类型、可空性和字符集等信息单独解读。Using filesort不代表一定使用磁盘排序。Using index通常表示覆盖索引访问,不应直接等同于"使用了索引就一定很快"。
7.2 使用 EXPLAIN ANALYZE
在 MySQL 8.0 支持的环境中,可以使用:
sql
EXPLAIN ANALYZE
SELECT id, status, create_time
FROM orders
WHERE user_id = 1001
AND status = 1
ORDER BY create_time DESC
LIMIT 20;
它会实际执行查询并展示执行过程中的实际行数和耗时等信息。
应在合适的环境中执行,并注意它与普通 EXPLAIN 的区别:EXPLAIN ANALYZE 不是纯粹的静态计划查看。
7.3 优化验证流程
- 记录原始 SQL 的执行耗时和执行计划。
- 分析查询条件、排序需求和现有索引。
- 设计候选联合索引。
- 在测试环境中建立索引并重新执行查询。
- 比较执行计划、扫描行数和实际耗时。
- 检查新增索引的存储成本与写入影响。
- 上线后继续观察慢 SQL 和索引使用情况。
八、场景总结
本场景的核心不是简单地给表增加索引,而是根据实际查询模式设计索引。
需要掌握的结论:
- 多个单列索引不一定能高效完成多条件过滤和排序。
- 联合索引应结合等值条件、范围条件、排序要求及查询频率进行设计。
- 最左前缀原则约束的是索引字段的连续前缀,不是
WHERE条件的书写顺序。 (a, b, c)可以利用WHERE b = ? AND a = ?中的a、b条件,因为条件同时覆盖了最左前缀(a, b)。(a, b, c)对WHERE a = ? AND c = ?通常只能利用a进行连续前缀定位,c不能越过缺失的b直接参与连续前缀定位。- 范围条件会影响后续字段参与索引定位的能力,但不意味着后续字段完全失效。
- 索引是否有效,应结合
EXPLAIN、EXPLAIN ANALYZE、实际耗时和数据分布判断。 - 索引并非越多越好,还需要权衡存储、写入和维护成本。
最终原则:围绕真实业务 SQL 设计索引,用执行计划和实际数据验证效果,而不是机械套用索引规则。