3. 索引基础回顾:B+树索引结构、聚簇索引与非聚簇索引、索引在查询中的作用

在深入慢查询复盘之前,必须重新夯实索引的底层原理。很多慢查询的根因,本质上是对索引结构、数据组织方式以及索引在查询执行路径中的角色理解不够透彻。本节从三个核心维度进行回顾:B+树结构、聚簇与非聚簇索引、索引在查询中的实际作用。

3.1 B+树索引结构

绝大多数关系型数据库(如 MySQL InnoDB、PostgreSQL)的索引底层采用 B+树。B+树是一种平衡多路搜索树,专为磁盘 I/O 优化设计,具有以下关键特征:

  • 非叶子节点只存储键值和指针:不存储实际数据行,因此每个节点可以容纳更多键值,降低树的高度。
  • 叶子节点存储完整键值和指向数据行的指针(或数据本身):所有叶子节点通过双向链表连接,形成有序链表,支持高效的范围扫描。
  • 所有数据行都在叶子节点:查询任何一条记录都需要从根节点走到叶子节点,路径长度(树高)相同,查询性能稳定。
  • 扇出(Fan-out)大:每个节点通常可存储数百个键值,3~4 层 B+树即可支撑数亿条记录。
关键指标: B+树的高度通常为 2~4 层。对于一张 1000 万行的表,树高为 3 时,一次索引查找仅需 3 次磁盘 I/O(根节点常驻内存,实际仅 2 次 I/O)。
B+树索引结构示意图 B+树索引结构(3层示例) 根节点 [50, 150] 内部节点 [10, 30] 内部节点 [70, 110] 内部节点 [180, 220] 叶子节点 [5, 8, 10] → 数据 叶子节点 [20, 25, 30] → 数据 叶子节点 [70, 85, 100] → 数据 叶子节点 [150, 180, 200] → 数据 叶子节点 [220, 250] → 数据 说明: • 非叶子节点(蓝色)只存储键值和子节点指针,不存数据。 • 叶子节点(绿色)存储完整键值及指向数据行的指针(或数据本身)。 • 红色虚线箭头表示叶子节点之间的双向链表,支持高效范围扫描。

3.2 聚簇索引与非聚簇索引

根据数据行在磁盘上的物理存储方式与索引的关系,索引分为两大类:聚簇索引(Clustered Index) 和 非聚簇索引(Non-Clustered Index,也称二级索引)。

对比维度 聚簇索引 非聚簇索引(二级索引)
数据存储位置 叶子节点直接存储整行数据 叶子节点存储主键值(或行指针)
每张表数量 只能有 1 个(InnoDB 主键即聚簇索引) 可以有多个
查询方式 直接通过索引找到数据行,无需回表 先找到主键值,再通过聚簇索引回表查询完整数据
范围查询性能 极优(数据物理连续) 一般(需要多次回表)
插入性能 可能产生页分裂(主键无序插入时) 影响较小
典型场景 主键查询、范围扫描、排序 非主键列的精确匹配、覆盖索引
InnoDB 引擎的特殊性: 如果表没有显式定义主键,InnoDB 会选择一个唯一非空索引作为聚簇索引;如果也没有,则隐式生成一个 6 字节的 ROWID 作为聚簇索引。建议始终为每张表显式定义自增主键,避免隐式 ROWID 带来的不可控性。
聚簇索引 vs 非聚簇索引查询路径 聚簇索引 vs 非聚簇索引查询路径对比 聚簇索引(主键索引) B+树索引(键值 = 主键) 叶子节点直接存储整行数据 直接返回数据行 无需回表,一次索引查找完成 非聚簇索引(二级索引) B+树索引(键值 = 非主键列) 叶子节点存储主键值 获取主键值后,回表查询 通过聚簇索引再次查找完整数据行 回表 关键区别: • 聚簇索引:一次 B+树查找即可获取完整数据行,性能最优。 • 非聚簇索引:需要两次 B+树查找(二级索引 + 回表),性能取决于回表次数。 • 覆盖索引可避免回表:如果二级索引的叶子节点已包含查询所需的所有列,则无需回表。

3.3 索引在查询中的作用

索引在查询执行过程中扮演多个关键角色,理解这些作用有助于在慢查询复盘时快速定位索引问题:

  1. 快速定位数据行:通过 B+树从根节点到叶子节点的路径查找,将全表扫描(O(n))降为索引查找(O(log n))。
  2. 避免全表扫描:当 WHERE 条件命中索引列时,数据库优先使用索引,避免逐行扫描整个表。
  3. 排序优化:如果 ORDER BY 列是索引的前缀列,B+树的有序性可以直接返回有序结果,避免额外的文件排序(filesort)。
  4. 覆盖索引(Covering Index):当查询所需的所有列都包含在同一个二级索引中时,无需回表,直接从索引叶子节点返回数据。
  5. 索引下推(Index Condition Pushdown, ICP):MySQL 5.6+ 特性,将 WHERE 条件中部分过滤条件下推到存储引擎层,在索引遍历过程中提前过滤,减少回表次数。
  6. 唯一性约束:唯一索引保证列值的唯一性,避免重复数据。
慢查询复盘中的索引检查清单:
✅ WHERE 条件列是否有索引?
✅ 索引列的顺序是否与查询条件匹配(最左前缀原则)?
✅ 是否使用了覆盖索引避免回表?
✅ ORDER BY 列是否在索引中?
✅ 索引选择性(Cardinality)是否足够高?
✅ 是否存在隐式类型转换导致索引失效?

3.4 索引选择性与最左前缀原则

两个核心概念直接影响索引是否被有效使用:

概念 定义 优化目标 反例
索引选择性 索引列中不同值的数量与总行数的比值(Cardinality / Rows) 选择性越接近 1,索引效果越好 性别列(只有 2 个值),选择性极低,索引效果差
最左前缀原则 联合索引 (a, b, c) 只能从最左侧列开始匹配查询条件 查询条件必须包含索引的最左列 WHERE b = 1 AND c = 2 无法使用索引 (a, b, c)
常见误区: 联合索引 (a, b, c) 可以用于 WHERE a = 1 AND c = 2(a 列过滤后,c 列无法利用索引有序性,但 a 列仍可走索引)。但 WHERE b = 1 完全无法使用该索引。设计联合索引时,应将选择性最高的列放在最左侧。

3.5 索引在查询执行计划中的表现

通过 EXPLAIN 命令可以观察索引在查询中的实际使用情况。以下是常见的索引访问类型(type 列),按性能从优到劣排列:

type 含义 性能评价
system 表只有一行(系统表) 最优
const 主键或唯一索引等值匹配 极优
eq_ref JOIN 时使用主键或唯一索引等值匹配 极优
ref 普通索引等值匹配 良好
range 索引范围扫描(BETWEEN、>、<、IN) 良好
index 全索引扫描(遍历整个索引树) 一般(比全表扫描好,但仍需优化)
ALL 全表扫描 最差,慢查询的主要来源
复盘建议: 在慢查询日志中,重点关注 type 为 ALL 或 index 的查询。对于 type 为 ref 或 range 的查询,检查 rows 列是否过大,以及 Extra 列是否出现 "Using filesort" 或 "Using temporary" 等需要优化的信息。