索引是数据库性能调优的核心手段。一个查询可能因为一条合适的索引从几十秒降到毫秒级,也可能因为一个错误的索引使用导致全表扫描拖垮整个库。本文从 B+Tree 索引的底层结构出发,结合 EXPLAIN 分析和真实案例,系统梳理 MySQL 索引优化的核心知识和实战技巧。
1. B+Tree 索引结构
MySQL 的 InnoDB 存储引擎使用 B+Tree 作为索引的底层数据结构。理解 B+Tree 是理解一切索引行为的基础。
B+Tree 相比普通 B-Tree 有几个关键特点:
- 非叶子节点只存储键值:不存储数据,因此每个节点可以容纳更多的键,树的高度更低
- 叶子节点存储数据:对于聚簇索引,叶子节点直接存储整行数据;对于二级索引,叶子节点存储主键值
- 叶子节点之间形成双向链表:支持高效的范围查询和排序操作
- 所有数据都在叶子节点:每次查询的 I/O 次数相对固定,查询性能稳定
一个三层 B+Tree 就能存储数千万条数据,这意味着大多数查询只需要 2-3 次磁盘 I/O,这就是索引能大幅提升查询性能的根本原因。
-- 查看 InnoDB 索引相关信息
SHOW INDEX FROM orders;
-- 查看表空间和索引页使用情况
SELECT
name,
page_no,
page_type,
number_records
FROM information_schema.INNODB_BUFFER_PAGE
WHERE table_name LIKE '%orders%';
2. 索引类型对比
MySQL 支持多种索引类型,不同的场景需要选择合适的索引结构。
| 索引类型 | 底层结构 | 适用场景 | 限制 |
|---|---|---|---|
| B+Tree | 平衡多路搜索树 | 范围查询、排序、最左前缀匹配、OLTP 场景 | 不支持非前缀模糊查询 |
| Hash | 哈希表 | 等值查询 (=, IN),Memory 引擎 | 不支持范围查询、排序、模糊匹配 |
| Full-text | 倒排索引 | 大文本字段的全文搜索、关键词匹配 | 不支持精确匹配和范围查询,索引维护成本高 |
在实际开发中,B+Tree 索引是最常用也最需要深入理解的类型。绝大多数 SQL 优化工作都围绕如何更好地利用 B+Tree 索引展开。
3. 最左前缀法则
最左前缀法则是联合索引使用的核心规则。假设我们在 (a, b, c) 三个字段上建立了联合索引,那么查询条件中必须从最左边的列开始匹配,索引才能生效。
-- 创建联合索引
CREATE INDEX idx_a_b_c ON user (a, b, c);
-- 走索引的查询
SELECT * FROM user WHERE a = 1; -- 使用 a
SELECT * FROM user WHERE a = 1 AND b = 2; -- 使用 a, b
SELECT * FROM user WHERE a = 1 AND b = 2 AND c = 3; -- 使用 a, b, c
SELECT * FROM user WHERE a = 1 ORDER BY b; -- 使用 a, b(排序)
SELECT * FROM user WHERE b = 2 AND a = 1 AND c = 3; -- 优化器自动调整顺序
-- 不走索引(或部分走索引)的查询
SELECT * FROM user WHERE b = 2; -- 跳过了 a,全表扫描
SELECT * FROM user WHERE c = 3; -- 跳过了 a, b,全表扫描
SELECT * FROM user WHERE a = 1 AND c = 3; -- a 走索引,c 无法走(跳过了 b)
理解最左前缀法则的关键在于:联合索引在 B+Tree 中是按字段顺序逐层排序的。先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。这就意味着跳过前面的字段直接使用后面的字段时,B+Tree 无法快速定位。
4. 索引失效场景
在实际开发中,即使表上有合适的索引,查询也可能不走索引。以下是常见的索引失效场景:
- 对索引列使用函数:
WHERE DATE(create_time) = '2026-05-01'应该改为WHERE create_time >= '2026-05-01' AND create_time < '2026-05-02' - 隐式类型转换:
WHERE phone = 13800138000(phone 是 varchar,需要类型转换导致索引失效) - LIKE 以 % 开头:
WHERE name LIKE '%关键字'无法使用索引,但WHERE name LIKE '关键字%'可以 - OR 条件中包含非索引列:
WHERE id = 1 OR status = 1(status 无索引则全表扫描) - 联合索引违反最左前缀:上面已经讨论过
- 数据分布不均匀:当优化器认为全表扫描比走索引更快时,会选择不走索引
经验:检查索引失效的最快方法是使用 EXPLAIN,看
type字段是否为ALL(全表扫描),以及key字段是否为 NULL。如果看到这两项,基本可以确定索引有问题。
5. 覆盖索引与索引下推
5.1 覆盖索引
覆盖索引是指查询所需的所有字段都包含在索引中,无需回表查询。这是索引优化中性价比最高的手段之一。
-- 假设有联合索引 idx_user (id, name, age, phone)
-- 覆盖索引查询:所有字段都在索引中
SELECT id, name, age FROM user WHERE name = '张三';
-- 非覆盖索引查询:需要回表
SELECT id, name, age, address FROM user WHERE name = '张三';
-- address 不在索引中,需要根据主键回到聚簇索引查询
判断是否使用了覆盖索引,看 EXPLAIN 输出中的 Extra 字段是否包含 Using index。如果包含,说明查询完全在索引中完成,不需要回表。
5.2 索引下推
索引下推是 MySQL 5.6 引入的优化特性。在没有索引下推之前,存储引擎通过索引查到数据后,需要将数据返回给 Server 层再进行过滤。有了索引下推后,可以在存储引擎层就过滤掉不符合条件的数据,减少回表次数。
举例来说,对于联合索引 (name, age) 和查询 WHERE name LIKE '张%' AND age = 25:
- 未使用索引下推:通过 name 前缀匹配到多条记录,逐一回表取出完整行,再判断 age
- 使用索引下推:在索引遍历过程中直接判断 age 条件,只有 age=25 的才回表
EXPLAIN 输出中 Extra 字段显示 Using index condition 就表示触发了索引下推。
6. EXPLAIN 执行计划分析
EXPLAIN 是 SQL 优化最核心的工具。通过它可以看到 MySQL 如何执行一条查询,从而找到性能瓶颈。
EXPLAIN SELECT
o.id,
o.order_no,
o.amount,
u.name
FROM orders o
JOIN user u ON o.user_id = u.id
WHERE o.create_time > '2026-01-01'
AND o.status = 1
ORDER BY o.amount DESC
LIMIT 20;
EXPLAIN 输出中需要重点关注的字段:
| 字段 | 重要值 | 说明 |
|---|---|---|
| type | const > eq_ref > ref > range > index > ALL | 访问类型,性能从左到右递减,ALL 是全表扫描 |
| key | 索引名 或 NULL | 实际使用的索引,NULL 表示未使用索引 |
| rows | 预估行数 | MySQL 预估需要扫描的行数,越小越好 |
| Extra | Using index / Using index condition / Using filesort / Using temporary | 额外信息,出现 filesort 或 temporary 通常需要优化 |
| key_len | 字节数 | 索引使用的长度,联合索引中判断使用了几个字段 |
常见的优化目标:让 type 达到 ref 或 range 级别,rows 尽可能小,Extra 不要出现 Using filesort 和 Using temporary。
7. 慢查询日志优化实战
慢查询日志是发现线上 SQL 问题的第一道防线。开启慢查询日志并设置合理的阈值,可以帮助我们主动发现需要优化的查询。
-- 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志(生产环境可动态开启)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录
SET GLOBAL log_queries_not_using_indexes = ON;
下面通过一个真实案例来演示优化过程。
场景:订单查询页面响应超过 5 秒,SQL 如下:
-- 原始慢查询
SELECT
o.order_no,
o.amount,
o.status,
o.pay_time,
u.name,
u.phone
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.create_time BETWEEN '2026-04-01' AND '2026-04-30'
AND o.status IN (1, 2, 3)
ORDER BY o.create_time DESC
LIMIT 30;
分析过程:
- 使用 EXPLAIN 发现 type 为 ALL,rows 高达 500 万,Extra 显示 Using filesort
- 发现 orders 表仅在主键上有索引,create_time 和 status 都没有索引
- 添加联合索引
(create_time, status)后,type 变为 range,rows 降到 8 万 - Extra 仍然显示 Using filesort,说明排序没有完全走索引
- 将索引调整为
(status, create_time)并利用排序特性,Extra 变为 Using index condition - 查询时间从 5.2 秒降到 0.08 秒
-- 优化后的索引
ALTER TABLE orders ADD INDEX idx_status_create_time (status, create_time);
-- 优化后的查询(利用索引排序)
SELECT
o.order_no,
o.amount,
o.status,
o.pay_time,
u.name,
u.phone
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.create_time BETWEEN '2026-04-01' AND '2026-04-30'
AND o.status IN (1, 2, 3)
ORDER BY o.create_time DESC
LIMIT 30;
优化要点总结:
- 联合索引的设计要同时兼顾 WHERE 条件和 ORDER BY 排序
- 优先将等值条件放在联合索引的前面,范围条件放在后面
- 如果 WHERE 条件和 ORDER BY 字段不同,考虑是否可以通过索引同时满足两者
- LIMIT 配合合适的索引可以大幅减少扫描行数
8. 总结
MySQL 索引优化不是背几条规则就能做好的,它需要理解底层原理并在实战中不断积累经验。这里分享几条核心原则:
- 理解 B+Tree 是基础:所有索引行为都能从 B+Tree 的结构中找到解释
- 用好 EXPLAIN:每次写完 SQL 都跑一遍 EXPLAIN,养成习惯
- 联合索引设计要谨慎:把区分度高的字段放在前面,等值条件优先于范围条件
- 覆盖索引是最实惠的优化:用空间换回表 I/O,性价比极高
- 关注慢查询日志:让数据告诉你哪些 SQL 需要优化,而不是凭感觉猜测
数据库索引优化是一个持续的过程。随着业务的发展和数据量的增长,曾经合理的索引设计可能会失效。定期分析慢查询、审查执行计划,才能让数据库始终保持高性能。