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)。
3.2 聚簇索引与非聚簇索引
根据数据行在磁盘上的物理存储方式与索引的关系,索引分为两大类:聚簇索引(Clustered Index) 和 非聚簇索引(Non-Clustered Index,也称二级索引)。
| 对比维度 | 聚簇索引 | 非聚簇索引(二级索引) |
|---|---|---|
| 数据存储位置 | 叶子节点直接存储整行数据 | 叶子节点存储主键值(或行指针) |
| 每张表数量 | 只能有 1 个(InnoDB 主键即聚簇索引) | 可以有多个 |
| 查询方式 | 直接通过索引找到数据行,无需回表 | 先找到主键值,再通过聚簇索引回表查询完整数据 |
| 范围查询性能 | 极优(数据物理连续) | 一般(需要多次回表) |
| 插入性能 | 可能产生页分裂(主键无序插入时) | 影响较小 |
| 典型场景 | 主键查询、范围扫描、排序 | 非主键列的精确匹配、覆盖索引 |
InnoDB 引擎的特殊性: 如果表没有显式定义主键,InnoDB 会选择一个唯一非空索引作为聚簇索引;如果也没有,则隐式生成一个 6 字节的 ROWID 作为聚簇索引。建议始终为每张表显式定义自增主键,避免隐式 ROWID 带来的不可控性。
3.3 索引在查询中的作用
索引在查询执行过程中扮演多个关键角色,理解这些作用有助于在慢查询复盘时快速定位索引问题:
- 快速定位数据行:通过 B+树从根节点到叶子节点的路径查找,将全表扫描(O(n))降为索引查找(O(log n))。
- 避免全表扫描:当 WHERE 条件命中索引列时,数据库优先使用索引,避免逐行扫描整个表。
- 排序优化:如果 ORDER BY 列是索引的前缀列,B+树的有序性可以直接返回有序结果,避免额外的文件排序(filesort)。
- 覆盖索引(Covering Index):当查询所需的所有列都包含在同一个二级索引中时,无需回表,直接从索引叶子节点返回数据。
- 索引下推(Index Condition Pushdown, ICP):MySQL 5.6+ 特性,将 WHERE 条件中部分过滤条件下推到存储引擎层,在索引遍历过程中提前过滤,减少回表次数。
- 唯一性约束:唯一索引保证列值的唯一性,避免重复数据。
慢查询复盘中的索引检查清单:
✅ WHERE 条件列是否有索引?
✅ 索引列的顺序是否与查询条件匹配(最左前缀原则)?
✅ 是否使用了覆盖索引避免回表?
✅ ORDER BY 列是否在索引中?
✅ 索引选择性(Cardinality)是否足够高?
✅ 是否存在隐式类型转换导致索引失效?
✅ 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" 等需要优化的信息。