MySQL 联合索引设计与索引失效

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:

  1. 根据 user_id = 1001 找到候选记录。
  2. 读取记录并检查 status = 1。
  3. 根据 create_time 排序。
  4. 返回前 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);

原理:

  1. 先根据 user_id 定位索引范围。
  2. 再根据 status 缩小范围。
  3. 最后利用 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);

原理:

  1. 根据 status = 1 定位状态对应的索引范围。
  2. 在该范围内根据 create_time 进行范围扫描。
  3. 利用 create_time 的索引顺序满足排序。
  4. 找到满足时间条件的记录后,返回所需结果。

这里需要注意:范围条件并不意味着索引中后续字段必然完全失效。它主要影响后续字段进一步参与索引定位的能力,其他优化能力仍需结合具体 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 会按照联合索引字段的顺序组织索引记录,其排序逻辑可以理解为:

  1. 首先按照 a 排序。
  2. 当 a 相同时,按照 b 排序。
  3. 当 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) 进行连续前缀定位。


五、联合索引字段顺序如何选择?

设计联合索引时,不能简单地认为"区分度最高的字段永远放第一位"。

应综合考虑以下因素:

  1. 查询模式:哪些字段经常一起出现在查询条件中?
  2. 等值与范围条件:通常优先安排等值过滤字段,再考虑范围字段。
  3. 排序需求 :索引顺序能否满足 ORDER BY,减少额外排序?
  4. 最左前缀复用:该索引能否同时服务其他高频查询?
  5. 数据分布:字段的选择性、数据倾斜程度以及不同条件组合下的实际扫描量。
  6. 写入与维护成本:索引越多,插入、更新、删除时的维护成本通常越高。

例如:

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 优化验证流程

  1. 记录原始 SQL 的执行耗时和执行计划。
  2. 分析查询条件、排序需求和现有索引。
  3. 设计候选联合索引。
  4. 在测试环境中建立索引并重新执行查询。
  5. 比较执行计划、扫描行数和实际耗时。
  6. 检查新增索引的存储成本与写入影响。
  7. 上线后继续观察慢 SQL 和索引使用情况。

八、场景总结

本场景的核心不是简单地给表增加索引,而是根据实际查询模式设计索引。

需要掌握的结论:

  1. 多个单列索引不一定能高效完成多条件过滤和排序。
  2. 联合索引应结合等值条件、范围条件、排序要求及查询频率进行设计。
  3. 最左前缀原则约束的是索引字段的连续前缀,不是 WHERE 条件的书写顺序。
  4. (a, b, c) 可以利用 WHERE b = ? AND a = ? 中的 a、b 条件,因为条件同时覆盖了最左前缀 (a, b)。
  5. (a, b, c) 对 WHERE a = ? AND c = ? 通常只能利用 a 进行连续前缀定位,c 不能越过缺失的 b 直接参与连续前缀定位。
  6. 范围条件会影响后续字段参与索引定位的能力,但不意味着后续字段完全失效。
  7. 索引是否有效,应结合 EXPLAIN、EXPLAIN ANALYZE、实际耗时和数据分布判断。
  8. 索引并非越多越好,还需要权衡存储、写入和维护成本。

最终原则:围绕真实业务 SQL 设计索引,用执行计划和实际数据验证效果,而不是机械套用索引规则。

相关推荐
catoop2 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
Web极客码2 小时前
我用 Claude 分析 Redis 哨兵日志,实现跨机房数据不一致自动对齐
数据库·redis·php
库库的数据库笔记2 小时前
国产信创数据库的MySQL兼容性深度对比解析
大数据·数据库·mysql·oceanbase·政务
姚远Oracle ACE3 小时前
为什么无法连接到 PostgreSQL?
数据库·postgresql
ss2733 小时前
AI时代首先数据库-PostgreSQL实战笔记 | 22 PostgreSQL实例与集群
数据库·笔记·postgresql
ss2733 小时前
AI时代首先数据库-PostgreSQL实战笔记 | 21 PostgreSQL进程与内存架构介绍
数据库·笔记·postgresql
Henry-SAP4 小时前
SAP PP模块全景概览
数据库
喜欢吃豆5 小时前
Better Auth 深度解析:从用户认证到企业级身份基础设施
数据库·架构·权限·多租户
其实防守也摸鱼5 小时前
Cylab在线靶场——CTF中的通用技能
linux·数据库·学习·算法·靶场·ctf·学习网络安全