MySQL 索引完全指南
目录
一、索引概述
1.1 什么是索引
索引是帮助 MySQL 高效获取数据的数据结构。可以把索引理解为书的目录,通过目录可以快速定位到需要的章节,而无需翻阅整本书。
核心要点:
- 索引是一种数据结构(通常是 B+Tree)
- 索引存储在磁盘上
- 索引以空间换时间,提高查询效率
1.2 索引的作用
✅ 主要优势:
- 大幅提升查询速度:将全表扫描 O(n) 优化为树查找 O(log n)
- 降低 IO 成本:减少磁盘读取次数
- 排序优化:利用索引的有序性避免 filesort
- 分组优化:加速 GROUP BY 操作
性能对比示例:
1 | 100 万条数据查询: |
1.3 索引的优缺点
✅ 优点:
- 大幅提高查询效率
- 加速排序和分组操作
- 保证数据唯一性(唯一索引)
❌ 缺点:
- 占用额外存储空间
- 降低写操作性能(INSERT/UPDATE/DELETE 需维护索引)
- 索引过多会影响优化器选择
权衡原则:
- 读多写少的表适合建索引
- 频繁更新的字段谨慎建索引
- 区分度低的字段(如性别)不适合建索引
二、索引分类体系
索引可以从多个维度进行分类,理解这些分类有助于我们更好地设计和使用索引。
2.1 按数据结构分类
| 索引类型 | 说明 | 适用场景 | MySQL 支持 |
|---|---|---|---|
| B+Tree | 平衡多路搜索树 | 范围查询、排序、分组 | ✅ InnoDB/MyISAM 默认 |
| Hash | 哈希表 | 等值查询 | ✅ Memory 引擎 |
| Full-Text | 全文索引 | 文本搜索 | ✅ MyISAM/InnoDB 5.6+ |
| R-Tree | 空间索引 | 地理空间数据 | ✅ MyISAM |
重点说明:
- MySQL InnoDB 和 MyISAM 引擎默认使用 B+Tree 索引
- Hash 索引仅支持精确匹配,不支持范围查询
- 全文索引用于大文本字段的关键词搜索
2.2 按物理存储分类
聚簇索引(Clustered Index)
定义:数据行存储在索引的叶子节点中,索引和数据在一起。
特点:
- ✅ InnoDB 的主键索引就是聚簇索引
- ✅ 每个表只能有一个聚簇索引
- ✅ 查询效率高(一次 IO 即可获取数据)
- ❌ 插入非连续数据会导致页分裂
示意图:
1 | B+Tree 叶子节点 → 直接存储数据行 |
二级索引(Secondary Index / 非聚簇索引)
定义:叶子节点存储的是主键值,需要通过主键再次查找数据(回表)。
特点:
- ✅ 一个表可以有多个二级索引
- ✅ 适合辅助查询条件
- ❌ 需要”回表”操作(二次查询)
示意图:
1 | B+Tree 叶子节点 → 存储主键值 → 通过主键查找聚簇索引 → 获取数据行 |
2.3 按逻辑用途分类
1. 普通索引(Normal Index)
最基本的索引类型,没有任何约束。
1 | -- 创建普通索引 |
特点:
- 可以包含重复值
- 可以包含 NULL 值
- 一个表可以有多个普通索引
2. 唯一索引(Unique Index)
索引列的值必须唯一,但允许有空值。
1 | -- 创建唯一索引 |
特点:
- ✅ 保证数据唯一性
- ✅ 允许 NULL 值(多个 NULL 不违反唯一性)
- ⚠️ 插入重复值会报错
3. 主键索引(Primary Key)
特殊的唯一索引,不允许有空值。
1 | CREATE TABLE users ( |
特点:
- ✅ 唯一且非空
- ✅ 每个表只能有一个主键
- ✅ InnoDB 中主键就是聚簇索引
4. 复合索引(Composite Index / 联合索引)
多个列组合成一个索引。
1 | -- 创建复合索引 |
特点:
- ✅ 遵循最左前缀原则
- ✅ 覆盖多个查询条件时效率高
- ⚠️ 索引列顺序很重要
最左前缀原则示例:
1 | -- 索引:idx_name_age (name, age, city) |
三、索引底层实现
3.1 Hash 索引
基于哈希表实现,通过哈希函数计算键值的哈希码。
工作原理:
1 | Key → Hash Function → Hash Code → Bucket → Data Pointer |
优点:
- ✅ 等值查询极快:O(1) 时间复杂度
- ✅ 实现简单
缺点:
- ❌ 不支持范围查询(
>,<,BETWEEN) - ❌ 不支持排序(ORDER BY)
- ❌ 不支持模糊查询(LIKE)
- ❌ 哈希冲突影响性能
适用场景:
- Memory 存储引擎
- 只需要等值查询的场景
3.2 B-Tree 索引
B-Tree(Balance Tree)是一种自平衡的多路搜索树。
特点:
- 所有节点(包括内部节点和叶子节点)都存储数据
- 每个节点可以有多个子节点
- 树保持平衡,所有叶子节点在同一层
缺点:
- ❌ 非叶子节点也存储数据,导致每个节点能存储的键值较少
- ❌ 树的高度相对较高,IO 次数较多
- ❌ 范围查询需要遍历多个分支
3.3 B+Tree 索引(MySQL 默认)
B+Tree 是 B-Tree 的改进版本,也是 MySQL InnoDB 和 MyISAM 引擎默认使用的索引结构。
核心改进:
数据只在叶子节点
- 非叶子节点只存储索引键值和指针
- 可以在同样大小的页中存储更多键值
- 降低树的高度,减少 IO 次数
叶子节点形成链表
- 所有叶子节点通过指针连接成双向链表
- 范围查询只需找到起始节点,然后顺序遍历
- 非常适合范围查询和排序
更高的扇出(Fan-out)
- 假设页大小为 16KB,每个键值 8 字节,指针 6 字节
- B+Tree 非叶子节点可存储约 1170 个键值
- 3 层 B+Tree 可存储约 1170³ ≈ 16 亿条记录
结构示意图:
1 | [Root Node] |
优势总结:
- ✅ 树的高度更低(通常 2-3 层),IO 次数少
- ✅ 范围查询效率高(叶子节点链表)
- ✅ 适合磁盘存储(顺序访问友好)
- ✅ 支持排序和分组操作
3.4 为什么选择 B+Tree
| 对比项 | B+Tree | B-Tree | Hash | 二叉树 | 红黑树 |
|---|---|---|---|---|---|
| 范围查询 | ✅ 优秀 | ⚠️ 一般 | ❌ 不支持 | ⚠️ 一般 | ⚠️ 一般 |
| 等值查询 | ✅ O(log n) | ✅ O(log n) | ✅ O(1) | ⚠️ O(n)~O(log n) | ✅ O(log n) |
| 排序支持 | ✅ 优秀 | ⚠️ 一般 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 |
| 树高度 | ✅ 低(2-3层) | ❌ 较高 | - | ❌ 可能很高 | ❌ 较高 |
| IO 效率 | ✅ 高 | ⚠️ 一般 | ⚠️ 一般 | ❌ 低 | ❌ 低 |
| 磁盘友好 | ✅ 优秀 | ⚠️ 一般 | ⚠️ 一般 | ❌ 差 | ❌ 差 |
选择 B+Tree 的核心原因:
- 降低 IO 次数:树高度低,每次查询只需 2-3 次磁盘 IO
- 范围查询优化:叶子节点链表使范围查询只需一次定位 + 顺序扫描
- 磁盘预读友好:顺序访问利用操作系统预读机制
- 稳定性好:自平衡特性保证性能稳定
四、InnoDB 索引实现
4.1 聚簇索引
InnoDB 的数据文件本身就是索引文件,采用聚簇索引组织数据。
特点:
- 表数据按照主键顺序存储
- 主键索引的叶子节点存储完整的数据行
- 如果没有显式定义主键,InnoDB 会选择第一个非空唯一索引
- 如果都没有,InnoDB 会生成一个隐藏的 row_id 作为聚簇索引
存储结构:
1 | 聚簇索引(主键索引): |
优势:
- ✅ 主键查询极快(一次 IO)
- ✅ 范围查询高效(物理存储有序)
- ✅ 排序操作无需额外排序
劣势:
- ❌ 插入非连续主键会导致页分裂
- ❌ 更新主键代价高(需要移动数据)
4.2 二级索引
除了聚簇索引外,其他索引都是二级索引。
特点:
- 叶子节点存储的是主键值,而非数据行
- 查询时需要”回表”:先查二级索引得到主键,再查聚簇索引得到数据
存储结构:
1 | 二级索引: |
查询流程示例:
1 | -- 假设有二级索引 idx_name(name) |
回表开销:
- 每次二级索引查询都需要额外的聚簇索引查找
- 如果需要大量回表,性能会下降
- 可以使用覆盖索引避免回表
4.3 MyISAM 对比
MyISAM 使用非聚簇索引,索引和数据完全分离。
存储结构:
1 | 索引文件 (.MYI): |
对比表格:
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 索引类型 | 聚簇索引 | 非聚簇索引 |
| 数据存储 | 与主键索引一起 | 独立文件 |
| 主键查询 | 一次 IO | 两次 IO |
| 事务支持 | ✅ 支持 | ❌ 不支持 |
| 外键支持 | ✅ 支持 | ❌ 不支持 |
| 崩溃恢复 | ✅ 支持 | ❌ 不支持 |
| 并发性能 | ✅ 行级锁 | ❌ 表级锁 |
| 适用场景 | OLTP 事务型 | 读多写少、无事务需求 |
建议:
- ✅ 现代应用优先使用 InnoDB
- ❌ MyISAM 已在 MySQL 8.0 中被移除
五、索引使用原则
5.1 最左前缀原则
对于复合索引 (a, b, c),查询条件必须从最左边开始匹配。
示例:
1 | -- 创建复合索引 |
原理:
- B+Tree 先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序
- 没有 a 的值,无法确定 b 和 c 的位置
最佳实践:
- 将区分度高的列放在前面
- 将经常用于等值查询的列放在前面
- 将范围查询的列放在后面
5.2 覆盖索引
查询的列都在索引中,无需回表。
示例:
1 | -- 假设有索引 idx_name_age(name, age) |
优势:
- ✅ 避免回表,减少 IO 次数
- ✅ 显著提升查询性能
如何判断:
1 | EXPLAIN SELECT name, age FROM users WHERE name = 'Tom'; |
最佳实践:
- 尽量让常用查询使用覆盖索引
- 不要在 SELECT * 时使用覆盖索引优化
- 合理设计复合索引以支持覆盖查询
5.3 索引下推(ICP)
MySQL 5.6 引入的优化技术,在存储引擎层进行条件过滤。
工作原理:
1 | 传统方式: |
示例:
1 | -- 假设有复合索引 idx_name_age(name, age) |
优势:
- ✅ 减少回表次数
- ✅ 减少网络传输
- ✅ 提升查询性能
启用方式:
1 | -- 默认启用 |
六、索引失效场景
6.1 常见失效情况
1. 违背最左前缀原则
1 | -- 索引:idx_abc(a, b, c) |
2. 在索引列上进行计算或函数操作
1 | -- 索引:idx_create_time(create_time) |
3. 类型隐式转换
1 | -- 索引:idx_phone(phone VARCHAR) |
4. LIKE 以通配符开头
1 | -- 索引:idx_name(name) |
5. OR 条件中包含未索引列
1 | -- 索引:idx_name(name) |
6. NOT、!=、<> 操作符
1 | -- 索引:idx_status(status) |
注意:MySQL 优化器在某些情况下仍会使用索引,需通过 EXPLAIN 确认。
7. IS NULL / IS NOT NULL
1 | -- 索引:idx_name(name) |
8. ORDER BY 与索引顺序不一致
1 | -- 索引:idx_name_age(name, age) |
6.2 如何避免
✅ 最佳实践:
使用 EXPLAIN 分析
1
2EXPLAIN SELECT * FROM users WHERE name = 'Tom';
-- 关注 type、key、rows、Extra 字段避免在索引列上使用函数
1
2
3
4
5
6-- ❌ 不好
WHERE DATE(create_time) = '2024-01-01'
-- ✅ 好
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00'注意数据类型匹配
1
2
3-- 确保查询值类型与字段类型一致
WHERE varchar_column = 'string_value' -- 不是 123
WHERE int_column = 123 -- 不是 '123'合理使用通配符
1
2
3-- 尽量避免前导通配符
WHERE name LIKE 'Tom%' -- ✅ 可以用索引
WHERE name LIKE '%Tom' -- ❌ 不能用索引优化 OR 条件
1
2
3
4-- 改写为 UNION
SELECT * FROM users WHERE name = 'Tom'
UNION
SELECT * FROM users WHERE age = 20;
七、最佳实践
7.1 为什么使用自增主键
结合 B+Tree 的特点,自增主键有以下优势:
优势 1:减少页分裂
1 | 自增主键插入: |
优势 2:减少数据移动
- 自增主键总是追加到末尾
- 不需要移动已有数据
- 提高插入性能
优势 3:提高缓存命中率
- 顺序插入使数据在磁盘上连续存储
- 利用操作系统预读机制
- 提高缓冲池命中率
对比实验:
1 | -- 测试 100 万条数据插入性能 |
建议:
- ✅ 优先使用自增整数作为主键
- ⚠️ 分布式系统可使用雪花算法生成递增 ID
- ❌ 避免使用 UUID 作为主键(除非有特殊需求)
7.2 索引设计建议
✅ 应该建索引的场景:
高频查询字段
1
2-- WHERE、JOIN、ORDER BY、GROUP BY 中的字段
CREATE INDEX idx_email ON users(email);区分度高的字段
1
2
3-- 区分度 = 不同值数量 / 总记录数
-- 手机号、身份证、邮箱等区分度高
-- 性别、状态等区分度低,不适合建索引外键字段
1
2-- JOIN 操作的关联字段
CREATE INDEX idx_order_user_id ON orders(user_id);排序和分组字段
1
2-- ORDER BY、GROUP BY 中的字段
CREATE INDEX idx_create_time ON orders(create_time);
❌ 不应该建索引的场景:
区分度低的字段
1
2-- 性别只有 2 个值,区分度 50%,不适合建索引
-- 状态字段如果只有几个值,也不适合频繁更新的字段
1
2-- 每次更新都需要维护索引,影响性能
UPDATE users SET login_count = login_count + 1;大文本字段
1
2
3-- TEXT、BLOB 类型字段不适合建完整索引
-- 可以使用前缀索引
CREATE INDEX idx_content_prefix ON articles(content(100));小表
1
2-- 数据量小于 1000 行的表,全表扫描更快
-- 索引反而增加维护成本
索引数量控制:
- 单表索引建议不超过 5 个
- 复合索引列数建议不超过 5 个
- 定期清理无用索引
7.3 性能调优技巧
1. 使用 Explain 分析执行计划
1 | EXPLAIN SELECT * FROM users WHERE name = 'Tom' AND age > 20; |
关键字段说明:
| 字段 | 含义 | 理想值 |
|---|---|---|
type |
访问类型 | system > const > eq_ref > ref > range > index > ALL |
key |
实际使用的索引 | 非 NULL |
rows |
扫描行数 | 越少越好 |
Extra |
额外信息 | Using index > Using where > Using temporary > Using filesort |
2. 优化慢查询
1 | -- 开启慢查询日志 |
3. 监控索引使用情况
1 | -- 查看索引统计信息 |
4. 定期维护索引
1 | -- 分析表,更新统计信息 |
5. 批量插入优化
1 | -- 关闭自动提交 |
八、总结
核心要点回顾
索引本质:帮助 MySQL 高效获取数据的数据结构(通常是 B+Tree)
索引分类:
- 按结构:B+Tree、Hash、Full-Text、R-Tree
- 按存储:聚簇索引、二级索引
- 按用途:普通索引、唯一索引、主键索引、复合索引
B+Tree 优势:
- 树高度低(2-3 层),IO 次数少
- 叶子节点链表,范围查询高效
- 磁盘友好,适合数据库场景
InnoDB 特性:
- 聚簇索引:主键索引叶子节点存储完整数据
- 二级索引:叶子节点存储主键值,需要回表
- 推荐使用自增主键
使用原则:
- 遵循最左前缀原则
- 善用覆盖索引避免回表
- 了解索引失效场景
最佳实践:
- 使用 EXPLAIN 分析查询
- 合理设计索引(区分度、选择性)
- 定期维护和优化
学习路线建议
1 | 入门阶段: |