核心答案

B+Tree 分支多、树高低,单次查询需要的磁盘 IO 较少;叶子节点按顺序连接,既适合等值查询,也适合范围扫描。

B+Tree 的结构优势

数据库页面一次可以保存大量索引键和子节点指针。相比二叉树,B+Tree 在相同数据量下树高更低。非叶子节点主要用于导航,数据集中在叶子节点,因此每层能容纳更多分支。

聚簇索引与二级索引

InnoDB 主键索引的叶子节点保存整行数据,也叫聚簇索引。二级索引叶子节点保存索引列和主键值,通过二级索引查询其他列时,可能还要根据主键回到聚簇索引,这个过程称为回表。

联合索引与最左匹配

联合索引按定义顺序排序。索引 (a, b, c) 可以高效支持以 a 开始的查询条件;跳过最左列后,通常无法直接利用完整索引顺序完成定位。

CREATE INDEX idx_user_status_time
ON orders(user_id, status, created_at);

常见索引失效场景

  • 对索引列进行函数或计算。
  • 隐式类型转换。
  • 联合索引未满足最左匹配。
  • 前导模糊查询,如 LIKE '%java'
  • 优化器判断全表扫描成本更低。

参考资料

核心考点清单

  • 聚簇索引叶子保存整行,二级索引叶子保存主键值。
  • 二级索引查询非覆盖列需要回表,覆盖索引可减少一次树查找。
  • 联合索引遵循最左前缀,范围条件会影响后续列的利用方式。
  • 索引存在不代表一定使用,优化器会根据统计信息估算成本。
  • 随机主键、过多索引和页分裂都会增加写入成本。

为什么不用普通二叉树或 Hash

B+Tree 分支多、树高低,一次页读取能获得大量键;叶子有序连接,兼顾等值、范围、排序和前缀查询。Hash 擅长等值定位,却不天然支持范围和排序。

高频追问与参考回答

追问 1:索引下推是什么?

存储引擎遍历二级索引时直接使用索引中的字段过滤,减少不满足条件记录的回表次数,但不一定减少索引扫描量。

追问 2:为什么索引会“失效”?

可能是不满足最左前缀、函数计算、隐式转换或返回比例过大。更准确地说,优化器认为其他路径更便宜,应结合 EXPLAIN ANALYZE 判断。

追问 3:为什么不能为每列都建索引?

每个索引都占空间并增加插入、更新、日志和缓存压力,应围绕高价值查询设计,定期清理冗余索引。

机制全景图

下面把「MySQL 为什么使用 B+Tree 索引?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。

flowchart LR
    A["根据条件构造搜索键"]
    A --> B["从 B+Tree 根节点下降"]
    B --> C["定位叶子页区间"]
    C --> D["回表或直接取覆盖列"]
    D --> E["按顺序返回结果"]

完整链路:从输入到结果

沿着「根据条件构造搜索键 → 从 B+Tree 根节点下降 → 定位叶子页区间 → 回表或直接取覆盖列 → 按顺序返回结果」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。

1. 根据条件构造搜索键

优化器先依据谓词和索引定义构造可用搜索边界,联合索引遵守最左前缀与范围截断规律。

2. 从 B+Tree 根节点下降

非叶子节点只保存键和子页指针,扇出高使数千万行通常只需少数层随机访问。

3. 定位叶子页区间

叶子页有序并通过双向链连接,范围扫描可顺序读取相邻记录。

4. 回表或直接取覆盖列

二级索引叶子保存主键,缺少目标列时要回聚簇索引;覆盖索引可消除这一步。

5. 按顺序返回结果

顺序与索引一致时可直接输出,反向或不匹配排序可能产生 filesort 与额外临时空间。

源码与实现定位

入口 阅读重点
storage/innobase/btr B+Tree 页搜索与分裂实现
EXPLAIN ANALYZE 实际扫描、回表与节点耗时

源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。

参数配置与可复现实验

CREATE INDEX idx_order_tenant_status_time
ON orders(tenant_id, status, created_at, id);

构造均匀与状态偏斜两组百万行数据,对比联合索引列顺序、覆盖与回表;必须记录 rows examined。

验证步骤与预期结果

1. 固定输入和基线

先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「扫描/返回」为主基线,记录值应满足「理想接近 1」;同时保存 EXPLAIN 实际扫描行数、回表次数,使后续变化能够回到同一时间轴比较。

2. 从实现入口确认路径

在「storage/innobase/btr」确认请求确实进入「B+Tree 页搜索与分裂实现」对应的实现,再沿「EXPLAIN ANALYZE」观察「实际扫描、回表与节点耗时」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。

3. 注入本文特有的失败模式

优先复现「函数或隐式转换让索引列失去可搜索性」,并把单一变量逐级放大,直到「扫描/返回」越过「>100」。随后再分别验证「低选择性索引回表成本高于全表扫」和「过多冗余索引拖慢写入」,三类故障分开执行,避免多个变量同时变化而无法归因。

4. 执行止损和根因修复

第一轮只应用「按真实谓词重排联合索引」,确认它能控制影响范围;第二轮应用「消除隐式类型转换」,验证核心链路恢复;最后落实「删除前用 invisible index 灰度」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。

5. 通过退出条件

实验只有同时满足三项才算通过:「扫描/返回」回到「理想接近 1」、「回表次数」回到「覆盖查询为 0」、「索引/数据体积」回到「按写预算」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。

量化基线

指标 样例基线/口径 风险线 结论
扫描/返回 理想接近 1 >100 索引边界差
回表次数 覆盖查询为 0 高选择性仍大量回表 补覆盖列
索引/数据体积 按写预算 索引总量>数据 删冗余

这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。

事故复盘:联合索引存在但查询仍扫描大量行

索引为 (tenant_id,status,created_at),查询只给 status 和时间,缺少第一列,无法建立连续搜索区间。补齐租户条件或按真实访问模式重建索引后,扫描行数从百万降到数百。

失败模式 首要证据 第一处置动作
函数或隐式转换让索引列失去可搜索性 EXPLAIN 实际扫描行数 按真实谓词重排联合索引
低选择性索引回表成本高于全表扫 回表次数 消除隐式类型转换
过多冗余索引拖慢写入 Buffer Pool 读页 删除前用 invisible index 灰度

发布与回滚检查点

  • 发布前:确认「storage/innobase/btr」对应实现和上述配置在目标版本仍然有效,并保存「扫描/返回」基线。
  • 灰度中:同时观察 EXPLAIN 实际扫描行数、回表次数、Buffer Pool 读页;任一指标越过表中风险线,就停止继续扩量。
  • 回滚时:先执行「按真实谓词重排联合索引」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
  • 发布后:至少覆盖一个完整峰值周期,确认「函数或隐式转换让索引列失去可搜索性」没有再次出现,才关闭变更观察窗口。

方案对比与选型

方案 更适合的场景 主要收益 代价与边界
单列索引 独立高选择性条件 定义简单、复用直接 难覆盖组合过滤与排序
联合索引 固定组合过滤和排序 一次定位并可覆盖查询 列顺序依赖访问模式
覆盖索引 热点查询返回列稳定且少 减少回表 I/O 索引更宽,写放大与缓存占用增加

选型至少带上 数据规模、选择性、读写比、事务长度和峰值并发,并用上面的量化基线验证;未知数据应明确为待测假设。

设计边界与工程取舍

索引是否有效由数据分布、过滤组合、返回列和排序共同决定;“建了索引”不是结论,必须用真实执行计划和 rows examined 验证。

工程落地遵循:正确性由约束和事务兜底,性能优化必须用执行计划与测量验证。回答时直接引用「storage/innobase/btr」、配置实验和事故数据,比复述固定模板更有说服力。