索引是数据库性能调优的核心手段。一个查询可能因为一条合适的索引从几十秒降到毫秒级,也可能因为一个错误的索引使用导致全表扫描拖垮整个库。本文从 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 达到 refrange 级别,rows 尽可能小,Extra 不要出现 Using filesortUsing 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;

分析过程

  1. 使用 EXPLAIN 发现 type 为 ALL,rows 高达 500 万,Extra 显示 Using filesort
  2. 发现 orders 表仅在主键上有索引,create_time 和 status 都没有索引
  3. 添加联合索引 (create_time, status) 后,type 变为 range,rows 降到 8 万
  4. Extra 仍然显示 Using filesort,说明排序没有完全走索引
  5. 将索引调整为 (status, create_time) 并利用排序特性,Extra 变为 Using index condition
  6. 查询时间从 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 需要优化,而不是凭感觉猜测

数据库索引优化是一个持续的过程。随着业务的发展和数据量的增长,曾经合理的索引设计可能会失效。定期分析慢查询、审查执行计划,才能让数据库始终保持高性能。