3. 索引原理:B+树数据结构、聚簇索引与非聚簇索引、索引的存储与检索过程
索引是数据库性能调优的核心武器,而理解其底层原理是写出高效SQL、避免慢查询的前提。本章从B+树数据结构出发,深入剖析聚簇索引与非聚簇索引的本质区别,并完整还原索引的存储与检索过程。
3.1 B+树:索引的物理骨架
几乎所有主流关系型数据库(MySQL InnoDB、PostgreSQL、Oracle)都采用B+树作为索引的底层数据结构。B+树是一种平衡多路搜索树,专为磁盘I/O优化设计。
3.1.1 B+树的核心特征
| 特性 | 说明 | 对性能的影响 |
|---|---|---|
| 多路平衡 | 每个节点可存储多个键值,树的高度通常为2~4层 | 极少的磁盘I/O次数,千万级数据只需3~4次I/O |
| 非叶子节点只存键 | 内部节点仅存放索引键和子节点指针,不存数据 | 节点可容纳更多键,降低树高 |
| 叶子节点存数据或指针 | 所有数据记录或指向记录的指针都存放在叶子节点 | 范围查询只需遍历叶子链表,无需回溯 |
| 叶子节点双向链表 | 叶子节点之间通过指针形成有序链表 | 支持高效的范围扫描和排序 |
| 自平衡 | 插入/删除后通过分裂、合并保持平衡 | 查询时间复杂度稳定在O(log n) |
面试高频点: B+树相比B树的优势在于:①非叶子节点不存数据,树高更低;②叶子节点链表支持高效范围查询;③所有查询路径长度一致,避免B树中数据在内部节点导致的随机I/O。
3.2 聚簇索引与非聚簇索引
InnoDB存储引擎中,索引分为聚簇索引和非聚簇索引(也称二级索引)。两者的根本区别在于数据行的物理存储方式。
3.2.1 聚簇索引(Clustered Index)
- 定义: 数据行的物理存储顺序与索引键的逻辑顺序一致。InnoDB中,主键索引就是聚簇索引。
- 存储方式: B+树的叶子节点直接存储完整的数据行。
- 特点:
- 一张表只能有一个聚簇索引(因为数据只能有一种物理排列顺序)。
- 如果没有定义主键,InnoDB会选择一个唯一非空索引作为聚簇索引;如果也没有,则隐式生成一个6字节的ROWID作为聚簇索引。
- 聚簇索引的叶子节点就是数据页,数据行按主键顺序排列。
3.2.2 非聚簇索引(Secondary Index)
- 定义: 索引键的逻辑顺序与数据行的物理存储顺序无关。所有非主键索引都是非聚簇索引。
- 存储方式: B+树的叶子节点存储索引键 + 主键值(InnoDB中),不存储完整数据行。
- 特点:
- 一张表可以有多个非聚簇索引。
- 通过非聚簇索引查询时,需要先找到主键值,再通过聚簇索引回表查询完整数据行(称为回表查询)。
- 如果查询的列全部在索引中(覆盖索引),则无需回表。
| 对比维度 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 叶子节点内容 | 完整数据行 | 索引键 + 主键值 |
| 每表数量 | 只能1个 | 可多个(通常建议3~5个以内) |
| 数据物理顺序 | 与索引键顺序一致 | 与索引键顺序无关 |
| 查询方式 | 直接定位数据行 | 先查主键,再回表 |
| 范围查询性能 | 极优(数据连续) | 较差(需要多次回表) |
| 插入性能 | 顺序插入极快,随机插入可能页分裂 | 插入时需维护索引树,有额外开销 |
| 典型场景 | 主键查询、范围查询 | 非主键列的条件查询、排序、去重 |
3.3 索引的存储与检索过程
3.3.1 索引的物理存储
在InnoDB中,索引和数据都存储在表空间(.ibd文件)中。表空间由多个页(Page)组成,默认每页16KB。
| 存储层级 | 说明 | 大小 |
|---|---|---|
| 表空间 | 逻辑存储结构的顶层,一个表对应一个表空间 | 随数据增长 |
| 段 | 分为索引段、数据段、回滚段等 | 逻辑概念 |
| 区 | 连续64个页组成一个区,用于空间管理 | 1MB(64×16KB) |
| 页 | InnoDB磁盘管理的最小单位,B+树的节点对应一个或多个页 | 16KB(默认) |
| 行 | 数据记录,存储在页内 | 可变 |
关键理解: B+树的一个节点通常对应一个页(16KB)。非叶子节点中,一个16KB的页可以存储约1000个键值对(假设键为8字节,指针为6字节)。因此,高度为3的B+树可以存储约1000×1000×16 = 1600万条记录。这就是为什么千万级表的主键查询只需要3次磁盘I/O。
3.3.2 索引检索过程详解
以 SELECT * FROM user WHERE id = 100 为例,聚簇索引的检索过程如下:
- 根节点加载: 从磁盘加载B+树根节点所在的页到内存(1次I/O)。
- 二分查找: 在根节点中通过二分查找定位到下一层节点的指针。
- 中间节点加载: 加载中间节点页(1次I/O),继续二分查找。
- 叶子节点加载: 加载叶子节点页(1次I/O),叶子节点中存储了完整的数据行。
- 返回数据: 从叶子节点中取出数据行返回给客户端。
对于非聚簇索引查询 SELECT * FROM user WHERE age = 25:
- 在age索引的B+树中执行上述过程,找到叶子节点中的主键值(如id=10)。
- 使用主键值10,在聚簇索引的B+树中再次执行检索过程(回表)。
- 从聚簇索引的叶子节点中取出完整数据行。
性能陷阱: 非聚簇索引查询需要两次B+树遍历(索引树 + 聚簇树)。如果查询返回大量行(如
WHERE age BETWEEN 20 AND 30 返回10万行),则会产生10万次回表I/O,性能急剧下降。此时应考虑覆盖索引或索引下推优化。
3.3.3 覆盖索引与索引下推
| 优化技术 | 原理 | 适用场景 | 示例 |
|---|---|---|---|
| 覆盖索引 | 查询所需的所有列都在同一个非聚簇索引中,无需回表 | 高频查询的列较少时 | SELECT age FROM user WHERE age=25(age索引已包含age列) |
| 索引下推 | 在索引遍历过程中,提前过滤不符合条件的行,减少回表次数 | 多条件查询(如 WHERE age>20 AND name LIKE 'A%') |
MySQL 5.6+ 自动优化,无需改SQL |
实战建议: 在慢查询优化中,优先检查
EXPLAIN 输出的 Extra 列。如果出现 Using index 表示使用了覆盖索引;如果出现 Using index condition 表示使用了索引下推。两者都是高效查询的标志。
3.4 索引设计原则与避坑
3.4.1 索引设计核心原则
- 高选择性列优先: 索引列的区分度越高,过滤效果越好。例如性别列(只有男/女)不适合建索引,而身份证号列适合。
- 最左前缀原则: 联合索引
(a, b, c)只对a、a+b、a+b+c的查询有效,跳过最左列会导致索引失效。 - 避免冗余索引:
(a, b)已经覆盖了(a)的功能,不需要再单独建(a)索引。 - 控制索引数量: 索引会降低写入性能(每次INSERT/UPDATE/DELETE都需要维护索引树),单表建议不超过5个索引。
- 主键选择: 使用自增整数或雪花ID作为主键,避免使用UUID等随机值(会导致频繁的页分裂和索引碎片)。
3.4.2 常见索引失效场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
WHERE age+1 = 25 |
索引列参与计算 | 改为 WHERE age = 24 |
WHERE name LIKE '%张' |
左模糊匹配 | 改为 WHERE name LIKE '张%' 或使用全文索引 |
WHERE name = '张三' OR age = 25 |
OR条件导致索引合并 | 改为 UNION 或建联合索引 |
WHERE name = '张三' AND age > 20(联合索引为(age, name)) |
违反最左前缀 | 调整索引顺序为(name, age) |
WHERE id IN (SELECT id FROM ...) |
子查询可能导致索引失效 | 改为JOIN或EXISTS |
总结: B+树是数据库索引的基石,理解其多路平衡、叶子链表、非叶子节点只存键等特性,是优化慢查询的理论基础。聚簇索引与非聚簇索引的存储差异直接决定了查询路径和I/O次数。在实际调优中,通过
EXPLAIN 分析执行计划,结合覆盖索引、索引下推等技术,可以显著减少回表次数,将查询性能提升1~2个数量级。