数据库性能优化是后端开发中永恒的话题,而索引优化则是其中最关键的一环。深入理解MySQL索引的底层原理,不仅能帮助我们写出更高效的查询,还能在出现问题时快速定位并解决。本文将从B+树的数据结构开始,逐步深入到索引的使用策略、慢查询分析方法,以及我在实际项目中遇到的优化案例。

一、MySQL索引的底层结构:B+树

1.1 为什么MySQL选择B+树

在理解索引优化之前,我们需要先了解MySQL为什么选择B+树作为索引的数据结构。常见的数据结构如哈希表、二叉搜索树、B树等,各有其适用场景:

B+树的优势在于:非叶子节点不存储数据,使得一个节点可以存储更多的键值,从而降低树高,减少磁盘I/O次数。同时,叶子节点通过双向链表连接,天然支持高效的范围查询和排序。

1.2 InnoDB的索引结构

InnoDB存储引擎使用B+树实现索引,主要有两种索引类型:

聚簇索引(Clustered Index)

聚簇索引的叶子节点存储的是完整的行数据。InnoDB表的数据本身就是按照聚簇索引的顺序存储的,因此每张表只能有一个聚簇索引。默认情况下,主键就是聚簇索引。如果没有定义主键,InnoDB会选择第一个非空唯一索引作为聚簇索引;如果也没有这样的索引,则会隐式创建一个6字节的rowid作为聚簇索引。

-- 创建表时指定主键
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL,
  email VARCHAR(100) UNIQUE,
  age INT,
  INDEX idx_age (age)
) ENGINE=InnoDB;

-- id列自动成为聚簇索引
-- email列上的UNIQUE约束会自动创建唯一索引
-- idx_age是普通二级索引

二级索引(Secondary Index)

二级索引的叶子节点存储的是索引列的值和对应的主键值。当通过二级索引查询时,MySQL首先找到对应的主键值,然后再通过主键值到聚簇索引中查找完整的行数据,这个过程称为「回表」。

为了减少回表操作,MySQL引入了「覆盖索引」的概念:如果查询的所有列都包含在索引中,MySQL可以直接从索引中获取所有需要的数据,无需回表。

-- 假设有索引 idx_name_email (name, email)
-- 以下查询可以使用覆盖索引,无需回表
SELECT name, email FROM users WHERE name = '张三';

-- 以下查询需要回表,因为需要获取age列
SELECT name, email, age FROM users WHERE name = '张三';

二、索引的设计原则

2.1 最左前缀原则

联合索引遵循最左前缀原则,即查询条件必须从索引的最左边开始连续匹配。如果跳跃了某个列,索引将不能被完全利用。

-- 创建联合索引
CREATE INDEX idx_name_age_email ON users(name, age, email);

-- 可以使用索引(匹配最左前缀)
SELECT * FROM users WHERE name = '张三';
SELECT * FROM users WHERE name = '张三' AND age = 25;
SELECT * FROM users WHERE name = '张三' AND age = 25 AND email = 'zhangsan@example.com';

-- 不能使用索引(跳过了name)
SELECT * FROM users WHERE age = 25;

-- 部分使用索引(只能使用name列)
SELECT * FROM users WHERE name = '张三' AND email = 'zhangsan@example.com';

2.2 索引选择性

索引的选择性是指不重复索引值与总记录数的比值。选择性越高,索引的区分度越好,查询效率越高。一般来说,选择性低于10%的列不适合建立索引。

-- 计算索引选择性
SELECT 
  COUNT(DISTINCT name) / COUNT(*) AS name_selectivity,
  COUNT(DISTINCT age) / COUNT(*) AS age_selectivity,
  COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity
FROM users;

-- 结果示例:name_selectivity = 0.95, age_selectivity = 0.30, gender_selectivity = 0.02
-- gender列的选择性太低,不适合单独建立索引

2.3 避免索引失效的常见场景

在实际开发中,有很多常见的写法会导致索引失效:

-- 1. 对索引列进行函数操作
SELECT * FROM users WHERE YEAR(created_at) = 2026;  -- 索引失效
SELECT * FROM users WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';  -- 索引有效

-- 2. 隐式类型转换
SELECT * FROM users WHERE phone = 13800138000;  -- phone是varchar,索引失效
SELECT * FROM users WHERE phone = '13800138000';  -- 索引有效

-- 3. 使用LIKE前缀模糊匹配
SELECT * FROM users WHERE name LIKE '%张三%';  -- 索引失效
SELECT * FROM users WHERE name LIKE '张三%';   -- 索引有效(前缀匹配)

-- 4. OR条件使用不当
SELECT * FROM users WHERE name = '张三' OR age = 25;  -- 可能索引失效
-- 优化:使用UNION
SELECT * FROM users WHERE name = '张三'
UNION ALL
SELECT * FROM users WHERE age = 25 AND name != '张三';

-- 5. 不等于和NOT IN
SELECT * FROM users WHERE age != 25;  -- 索引可能失效,取决于数据分布

-- 6. 索引列参与计算
SELECT * FROM users WHERE age + 1 = 26;  -- 索引失效
SELECT * FROM users WHERE age = 25;      -- 索引有效

三、慢查询分析与优化

3.1 启用慢查询日志

慢查询日志是定位性能问题的重要工具。可以通过以下配置开启:

-- 临时开启
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 记录执行时间超过1秒的查询
SET GLOBAL log_queries_not_using_indexes = 'ON';  -- 记录未使用索引的查询

-- my.cnf永久配置
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

3.2 EXPLAIN分析执行计划

EXPLAIN是分析SQL执行计划的核心工具。通过分析EXPLAIN的输出,我们可以了解MySQL是如何执行查询的。

EXPLAIN SELECT * FROM users WHERE name = '张三' AND age > 20;

关键字段解读:

3.3 一个真实的优化案例

在我们的订单系统中,有一个查询需要按创建时间倒序分页查询某个用户的订单:

-- 原始查询
SELECT * FROM orders 
WHERE user_id = 123 
ORDER BY created_at DESC 
LIMIT 100000, 10;

这个查询在数据量大时非常慢,EXPLAIN显示type为index,rows超过100万。问题出在两个方面:

  1. LIMIT 100000, 10需要扫描前100010条记录,然后丢弃前100000条
  2. ORDER BY created_at DESC需要对所有记录进行排序

优化方案:

-- 方案1:使用覆盖索引优化
-- 创建覆盖索引
CREATE INDEX idx_user_created_id ON orders(user_id, created_at, id);

-- 先查询id,再回表查询完整数据
SELECT * FROM orders 
WHERE id IN (
  SELECT id FROM orders 
  WHERE user_id = 123 
  ORDER BY created_at DESC 
  LIMIT 100000, 10
);

-- 方案2:使用延迟关联(更优)
SELECT o.* 
FROM orders o
INNER JOIN (
  SELECT id 
  FROM orders 
  WHERE user_id = 123 
  ORDER BY created_at DESC 
  LIMIT 100000, 10
) tmp ON o.id = tmp.id;

-- 方案3:使用书签记录上一页的最后一条记录(最佳)
-- 上一页最后一条记录 created_at = '2026-04-01 12:00:00', id = 99999
SELECT * FROM orders 
WHERE user_id = 123 
  AND (created_at < '2026-04-01 12:00:00' 
       OR (created_at = '2026-04-01 12:00:00' AND id < 99999))
ORDER BY created_at DESC, id DESC
LIMIT 10;

最终采用方案3,查询耗时从原来的3.2秒降低到10毫秒。

四、索引维护与监控

4.1 索引的维护

索引并非创建后就一劳永逸,需要定期维护:

-- 查看索引使用情况
SELECT 
  table_name,
  index_name,
  cardinality,
  rows_selected,
  rows_inserted
FROM information_schema.statistics
WHERE table_schema = 'your_database';

-- 分析表和索引统计信息
ANALYZE TABLE orders;

-- 优化表(会锁表,谨慎使用)
OPTIMIZE TABLE orders;

-- 查看索引碎片情况
SELECT 
  table_name,
  index_name,
  ROUND(data_free / 1024 / 1024, 2) AS free_mb
FROM information_schema.tables
WHERE table_schema = 'your_database' 
  AND data_free > 0;

4.2 索引优化检查清单


MySQL索引优化是一门需要理论基础和实践经验相结合的学问。理解B+树的结构和工作原理,掌握EXPLAIN的使用方法,遵循索引设计的基本原则,再加上定期的监控和维护,才能真正发挥索引的最大价值。希望本文的分享能帮助你在实际项目中写出更高效的SQL。