MySQL索引原理与B+树结构
MySQL 是业务系统中最常见的数据库,但用好它并不容易。索引设计、SQL 优化、事务处理都是日常开发中需要关注的点。本文从实际场景出发,讲常见问题和解决思路。
一、为什么需要索引
#
1.1 无索引的查询代价
假设有一张用户表,包含100万条记录:
SELECT * FROM user WHERE id = 500000; |
如果没有索引,MySQL需要逐行扫描(全表扫描),直到找到匹配记录。时间复杂度为O(n)。
#
1.2 索引的本质
索引是一种数据结构,用于快速定位数据。MySQL的InnoDB引擎使用B+树作为默认索引结构。
二、B树与B+树
#
2.1 二叉搜索树的局限
普通二叉搜索树在极端情况下会退化为链表,查找效率变为O(n)。即使使用AVL树或红黑树保持平衡,由于每个节点只存储一个关键字,当数据量巨大时,树的高度仍然很高,导致磁盘I/O次数过多。
#
2.2 B树(Balanced Tree)
B树是一种多路平衡搜索树,每个节点可以存储多个关键字:
- 每个节点最多有m个子节点(m阶B树)
- 除根节点外,每个节点至少有m/2个子节点
- 所有叶子节点在同一层
[30, 60] |
B树的每个节点都存储数据,导致:
- 单个节点能存储的键值对减少
- 树的高度相对较高
- 范围查询需要中序遍历,效率低
#
2.3 B+树的优势
B+树是B树的改进版,也是MySQL索引的实际实现:
结构特点:
- 非叶子节点只存储键值,不存储数据
- 所有数据都存储在叶子节点
- 叶子节点之间通过指针相连,形成有序链表
[30 | 60] |
核心优势:
- 更矮更胖:非叶子节点不存数据,单个节点容纳更多键,树高更低
- 顺序访问:叶子节点链表连接,范围查询只需遍历链表
- 稳定性能:所有查询都到达叶子节点,IO次数一致
三、InnoDB的B+树实现
#
3.1 页(Page)结构
InnoDB以页为单位管理数据,默认每页16KB:
+-----------------+ |
#
3.2 聚簇索引(Clustered Index)
InnoDB表数据本身就是按B+树组织的索引结构,称为聚簇索引:
- 叶子节点存储完整的行数据
- 数据按主键顺序物理存储
主键B+树 |
特点:
- 一个表只能有一个聚簇索引(主键)
- 查询主键效率最高
- 插入按主键顺序最快(避免页分裂)
#
3.3 二级索引(Secondary Index)
非主键索引都是二级索引:
- 叶子节点存储:索引列 + 主键值
- 查询时需要”回表”:先查二级索引获取主键,再查聚簇索引获取完整数据
name列的二级索引 |
#
3.4 覆盖索引
如果查询的所有列都在二级索引中,无需回表:
-- 假设有联合索引(name, age) |
四、索引页分裂与合并
#
4.1 页分裂(Page Split)
当插入数据导致页满时,触发页分裂:
- 申请一个新页
- 将原页约一半数据移动到新页
- 更新父节点的指针
插入前: |
性能影响:
- 页分裂涉及多个页的修改,成本较高
- 频繁页分裂导致页空间利用率降低(通常50%左右)
- 数据物理顺序变得不连续,影响顺序读性能
#
4.2 页合并(Page Merge)
删除数据导致页利用率过低时(默认低于50%),可能触发页合并:
删除前: |
#
4.3 顺序主键的优势
使用自增ID作为主键:
- 新数据总是追加到最后一页
- 几乎不会发生页分裂
- 数据物理连续,顺序读取效率高
使用UUID作为主键:
- 插入位置随机
- 频繁触发页分裂
- 数据物理分散,随机IO多
五、索引的物理存储
#
5.1 表空间文件
InnoDB数据存储在表空间文件中:
ibdata1 # 系统表空间(共享) |
配置独立表空间:
[mysqld] |
#
5.2 索引与数据的关系
-- 查看表空间信息 |
#
5.3 索引高度估算
假设:
- 页大小:16KB
- 主键类型:BIGINT(8字节)
- 指针大小:6字节
- 每行数据:1KB
非叶子节点:
- 每个键值对大小:8 + 6 = 14字节
- 每页可容纳:16KB / 14B ≈ 1170个键
叶子节点:
- 每页可容纳:16KB / 1KB = 16条记录
树高计算:
- 高度1(根节点):1170个指针
- 高度2:1170 × 1170 = 136万指针
- 高度3:1170 × 1170 × 16 ≈ 2190万条记录
结论:2000万数据的表,树高通常只有3层,最多3次IO即可定位数据。
六、索引使用最佳实践
#
6.1 选择合适的列建索引
适合建索引的列:
- WHERE条件中频繁使用的列
- JOIN关联条件中的列
- ORDER BY / GROUP BY的列
- 区分度高的列(唯一值比例高)
不适合建索引的列:
- 区分度极低的列(如性别)
- 频繁更新的列(维护索引成本高)
- 小表(全表扫描更快)
- 很长的字符串列
#
6.2 索引设计原则
- 最左前缀原则:联合索引(a,b,c),查询条件必须包含最左边的列
- 避免冗余索引:已有(a,b),不需要单独的(a)
- 控制索引数量:单表不超过5个索引,索引过多影响写入性能
- 优先使用覆盖索引:减少回表次数
#
6.3 索引失效场景
-- 1. 使用函数 |
七、查看索引信息
#
7.1 查看表索引
SHOW INDEX FROM user; |
#
7.2 查看索引统计
-- 查看索引 cardinality(区分度) |
#
7.3 分析索引使用情况
-- 查看查询是否使用索引 |
八、总结
| 概念 | 说明 |
|---|---|
| B+树 | 多路平衡树,非叶子节点只存键,叶子节点存数据并双向链表连接 |
| 聚簇索引 | 数据与索引在一起,叶子节点存完整行数据,按主键排序 |
| 二级索引 | 叶子节点存索引列+主键,需要回表 |
| 覆盖索引 | 查询列全在索引中,无需回表 |
| 页分裂 | 页满时分裂成两页,影响插入性能 |
| 页合并 | 页利用率过低时合并,回收空间 |
理解B+树的结构特点,有助于:
- 合理设计表结构和主键
- 优化索引设计
- 写出高效SQL
- 诊断性能问题
核心要点
索引设计的原则:最左前缀原则、避免索引列参与计算、选择合适的索引类型
SQL 优化的方法:使用 EXPLAIN 分析执行计划、避免 SELECT *、优化 JOIN
事务隔离级别的选择:根据业务需求选择合适的级别
常见的锁问题:行锁、表锁、死锁的处理
总结
MySQL 优化是一个持续的过程,需要结合业务特点和数据量来调整。在实际项目中,定期分析慢查询日志、优化索引、调整配置参数,都能提升数据库性能。