4、索引原理与B+树结构

索引是数据库性能调优的核心武器。要理解为什么索引能加速查询,必须先深入其底层数据结构——B+树。本节将拆解B+树的结构、聚簇与非聚簇索引的差异、索引选择性以及复合索引的最左前缀原则,帮助你从原理层面掌握索引设计。

4.1 B+树数据结构

B+树是一种多路平衡查找树,是MySQL InnoDB存储引擎默认的索引结构。与二叉树或B树不同,B+树将所有数据存储在叶子节点,非叶子节点只存储键值和指针,不存储实际数据。这种设计带来了两个关键优势:

  • 树高更低:每个节点可存储大量键值(通常一个节点对应一个磁盘页,约16KB),使得三层B+树即可支撑千万级数据,查询只需3~4次磁盘I/O。
  • 范围查询高效:叶子节点通过双向链表连接,支持顺序扫描和范围查找,无需回根节点重新遍历。
关键指标:InnoDB中B+树每个节点默认16KB。假设主键为BIGINT(8字节)+ 指针(6字节),每个节点可存储约1170个键值对。三层B+树可存储约 1170 × 1170 × 16 = 2190万条记录。
B+树三层结构示意图 B+树三层结构示意图 根节点 [50, 150] 中间节点 [10, 30] 中间节点 [70, 110] 中间节点 [170, 200] 叶子节点 [1,5,8,10] 叶子节点 [20,25,30] 叶子节点 [70,85,110] 叶子节点 [150,170,200] 说明: • 非叶子节点(蓝/紫色)只存储键值和指针,不存数据。 • 叶子节点(绿色)存储完整数据行或主键值,并通过双向链表连接。 • 查询从根节点开始,逐层向下,最终在叶子节点找到目标数据。

4.2 聚簇索引与非聚簇索引

InnoDB中,索引分为聚簇索引和非聚簇索引(也称二级索引)。它们的核心区别在于数据存储位置和索引结构。

对比维度 聚簇索引 非聚簇索引(二级索引)
数据存储 叶子节点直接存储完整数据行 叶子节点存储主键值(或行指针)
数量限制 每张表只能有一个 每张表可以有多个
默认主键 InnoDB自动以主键创建聚簇索引 用户手动创建的普通索引
查询回表 直接返回数据,无需回表 需要根据主键回聚簇索引查询完整数据
范围查询 高效,叶子节点有序且连续 相对低效,需多次回表
实践建议:在InnoDB中,建议为每张表显式定义自增主键。自增主键能保证新插入的数据在B+树末尾追加,减少页分裂和碎片。如果使用UUID作为主键,随机写入会导致频繁的页分裂,严重影响写入性能。

4.3 索引选择性

索引选择性是指索引列中不同值的数量与总行数的比值。选择性越高,索引的过滤效果越好,查询效率越高。

  • 计算公式:选择性 = COUNT(DISTINCT column) / COUNT(*)
  • 理想值:接近1(如主键、唯一键)
  • 低选择性示例:性别列(只有男/女),选择性约0.5,索引效果差
  • 高选择性示例:用户邮箱、身份证号,选择性接近1
避坑提醒:不要对低选择性的列单独建索引。例如在性别列上建索引,查询时可能仍需要扫描大量数据,甚至被优化器放弃使用索引。低选择性列更适合作为复合索引的后缀列,而非前缀列。

4.4 复合索引最左前缀原则

复合索引(多列联合索引)遵循最左前缀原则:查询条件必须从索引的最左列开始,并且不能跳过中间的列,否则索引无法被完全使用。

假设在 (a, b, c) 三列上创建复合索引,索引的B+树会先按a排序,a相同再按b排序,b相同再按c排序。以下查询能使用索引的情况:

查询条件 是否使用索引 说明
WHERE a = 1 ✅ 完全使用 从最左列开始
WHERE a = 1 AND b = 2 ✅ 完全使用 连续使用前两列
WHERE a = 1 AND b = 2 AND c = 3 ✅ 完全使用 全部命中
WHERE b = 2 ❌ 无法使用 跳过了最左列a
WHERE a = 1 AND c = 3 ⚠️ 部分使用 只能用到a列,c列无法利用索引排序
WHERE a > 1 AND b = 2 ⚠️ 部分使用 a列范围查询后,b列无法继续使用索引
复合索引最左前缀匹配流程 复合索引 (a, b, c) 最左前缀匹配流程 查询条件:WHERE a=1 AND b=2 步骤1:是否包含最左列 a? 步骤2:是否连续包含列 b? ✅ 完全使用索引 (a, b) 注意:如果跳过b列直接查询c列(如 WHERE a=1 AND c=3),则只能用到a列,c列无法利用索引排序。

4.5 索引设计实战要点

  • 优先高选择性列:将选择性高的列放在复合索引的最左侧,最大化过滤效果。
  • 避免冗余索引:如果已有 (a, b) 索引,单独建 (a) 索引是冗余的,因为最左前缀已覆盖。
  • 覆盖索引优化:如果查询的所有列都在索引中,则无需回表,称为“覆盖索引”。例如 SELECT a, b FROM t WHERE a = 1 且索引为 (a, b)。
  • 排序与索引:ORDER BY 也能利用索引排序,但必须遵循最左前缀。例如索引 (a, b) 可以优化 ORDER BY a, b,但无法优化 ORDER BY b, a。
Go Web服务实战提示:在Go的ORM框架(如GORM、SQLx)中,通过 EXPLAIN 分析查询计划时,重点关注 type 字段(const/ref/range 为优,ALL 为全表扫描)和 Extra 字段(Using index 表示覆盖索引,Using filesort 表示需要额外排序)。结合本节B+树原理,可以快速定位索引失效原因。