B+ 树索引
B+ 树的页结构、InnoDB 回表、联合索引与写入开销。
页与树高
MySQL 8.0 InnoDB 以页为单位管理数据。查找既要比较键,也要取得对应页面。命中缓冲池时访问内存,未命中时可能需要存储 I/O。
平衡二叉树每层的分支少,查找路径可能经过较多分散的页面。B+ 树在一个节点中保存多个导航项,一次页访问就能排除大范围的记录,因而树高较低。
InnoDB 默认页大小为 16 KiB,也支持其他页大小。每页能容纳的分支数取决于键宽度、记录开销和填充情况。假设有效扇出为 500,两次分支选择可覆盖约 25 万个叶页;记录总量还取决于每个叶页能存多少记录。
B+ 树结构与查找过程
B+ 树非叶节点用于导航,叶层按键顺序组织记录。等值查找从根页开始,根据分隔键选择子页,逐层缩小范围,最终在叶页定位目标。每一层仍有页内查找成本,但较高扇出减少了需要访问的层数。
查找 id=42 时,根页和中间页选择包含该键的范围,叶页确认记录是否存在。上层页面少且访问频繁,容易命中缓存;叶页的命中率更受工作集大小和访问分布影响。三层树的页面可能都在内存中。
范围查询先定位起点,再沿有序叶层扫描,直到越过上界。这样不必为每条记录重新从根页查找。范围越大,需要扫描的叶页和处理的结果也越多。
InnoDB 记录组织与回表
InnoDB 聚簇索引叶记录包含行数据,二级索引记录包含索引列及主键值。经二级索引确定候选记录后,若查询还需要其中没有保存的列,就需要根据主键访问聚簇索引。这一额外的行访问通常称为回表。
CREATE TABLE demo_orders (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
note VARCHAR(200),
INDEX idx_tenant_time (tenant_id, created_at)
) ENGINE=InnoDB;
EXPLAIN SELECT id, created_at FROM demo_orders
WHERE tenant_id = 7 AND created_at >= '2026-09-01';
EXPLAIN SELECT id, created_at, note FROM demo_orders
WHERE tenant_id = 7 AND created_at >= '2026-09-01';第一条查询返回 id 和 created_at,idx_tenant_time 已包含所需列;第二条增加了 note,使用这个二级索引时还需要回表获取它。
索引包含某次查询所需的全部列时,就能覆盖该查询。增加返回列可能失去覆盖效果。小表或筛选后剩余行数较多时,优化器也可能选择其他访问方式,可通过执行计划查看。
二级索引记录还保存主键,因此较宽的主键会增大所有二级索引,减少每页记录数,并占用更多缓存。
联合索引与范围查询
联合索引按完整键的字典序排列。对于 (tenant_id, created_at),同一租户的记录先集中在相应键范围内,再按时间排列。固定 tenant_id 后查询时间区间,可以将前缀定位与范围扫描结合起来。
created_at 只在同一租户内有序。跨租户按时间排序时,这个索引的顺序无法直接满足要求。
返回列和行数也影响查询成本。只取必要字段可能形成覆盖访问,增加字段可能引入回表,大结果集则增加扫描和传输量。
| 查询选择 | 可能节省的工作 | 代价 |
|---|---|---|
| 只取所需列 | 避免传输无用数据,可能形成覆盖 | 需要明确调用方实际需求 |
| 扩宽二级索引 | 可能减少回表 | 索引更大,写入维护更多 |
| 返回大量行 | 范围定位仍可用 | 扫描、回表与传输仍随数据量增加 |
索引的写入开销
索引改善读路径,同时增加写入维护。插入需要更新聚簇索引及相关二级索引;页空间不足时可能发生分裂;索引列更新会改变相应记录。额外索引因而同时占用存储、缓冲空间和写入资源。
顺序主键通常让插入集中在相邻页面,随机键会让写入分散到更多页面。页分裂是否发生取决于页面剩余空间,写入速度还受并发和负载影响。
设计索引时,先看查询如何过滤、排序以及返回哪些列,再用执行计划检查扫描范围和回表情况。增加索引节省的读开销,还需要与读写比例和维护成本一起比较。