MySQL的索引并不是什么神秘组件,它本质是一套建立在磁盘页之上的有序数据结构。InnoDB存储引擎默认使用B+树来组织索引,之所以这样选,是因为数据库面对的是以磁盘为介质的持久化存储,每一次随机IO的代价都远高于内存计算。理解索引底层,要先抛开“索引就是加快查询”的笼统认知,去看它如何把查找过程中的磁盘访问次数压到最低。

一、为什么是B+树而不是其他结构
在众多数据结构中,哈希表、二叉搜索树、红黑树、B树都具备一定的检索能力,但放到数据库场景里各有短板。哈希表虽然等值查询是O(1),但它完全不支持范围查询,也无法利用“最左前缀”做排序,而业务里“查本月订单”这种范围语句无处不在。红黑树属于二叉树,每个节点最多两个分支,当数据量到千万级时树高会超过二十层,意味着一次查询可能要二十次磁盘IO,这显然不可接受。
B树虽然已经做了多路化,把多个键值塞进一个节点以减少高度,但它的非叶子节点也保存了完整行数据或数据指针,导致单个页能容纳的键值变少,树还是会偏高。B+树在此基础上做了关键改造:非叶子节点只保存键值和指向下层页面的指针,不存实际数据;所有真实记录都集中在叶子节点,并且叶子节点之间通过双向链表相连。这样非叶子节点能装下更多键,三层B+树就能索引上千万行,范围扫描只需顺着链表走。
二、InnoDB页与B+树的物理映射
InnoDB管理磁盘的最小单位是页(page),默认大小16KB。无论是表数据还是索引,都以页为单位读写。一个B+树节点就对应一个索引页,页内记录按照主键顺序排列,通过槽位(slot)做二分查找定位。当插入数据导致页满,就会触发页分裂,把一半记录移到新页,并在父节点增加指向新页的指针,以此维持平衡。
下面用伪代码描述一次简化的B+树节点分裂逻辑,帮助理解底层行为:
// 当一个索引页装满后触发分裂
public Page splitPage(Page fullPage) {
Page newPage = allocateNewPage();
// 把后半部分记录迁移到新页
List<Record> records = fullPage.getRecords();
int mid = records.size() / 2;
for (int i = mid; i < records.size(); i++) {
newPage.insert(records.get(i));
fullPage.remove(records.get(i));
}
// 父节点记录新页的最小键与指针
fullPage.getParent().insertKey(newPage.getMinKey(), newPage.getPointer());
return newPage;
}
聚簇索引(clustered index)在InnoDB里就是主键B+树,叶子节点直接挂的是整行数据。如果表没显式定义主键,InnoDB会选第一个唯一非空索引,若还没有就隐式生成6字节行ID。二级索引的叶子节点存的是主键值,而不是行地址,所以通过二级索引查非索引列时,必须拿主键再去聚簇索引走一遍,这个过程叫回表。
三、查询路径与执行代价分析
假设执行语句 select * from user where id = 123,InnoDB会从聚簇索引的根页开始,依据每层节点里的键值范围决定下一步访问哪个子页,直到抵达叶子页找到记录。由于树高通常只有三到四层,一次主键查询只需三四次IO。若是二级索引查询且要回表,则先搜二级索引树拿到主键,再搜聚簇索引树,IO次数大约翻倍。
我们可以通过对比来看不同索引设计的差异:
| 结构类型 | 范围查询 | 单层扇出 | 典型树高(千万数据) |
|---|---|---|---|
| 红黑树 | 不支持有序链扫 | 2 | 20层以上 |
| B树 | 中序遍历较麻烦 | 较低 | 5到7层 |
| B+树 | 叶子链表顺扫 | 高 | 3到4层 |
从表中能看出,B+树用更高的扇出压低了树高,又用叶子链表补齐了范围查询短板。这也是为什么MySQL把B+树作为唯一默认索引结构。
四、常见误区与优化建议
不少人在设计表时随意用UUID做主键,这会让聚簇索引的插入变成大量随机页写入,频繁触发页分裂,性能远不如自增主键的顺序追加。还有人以为 like '%abc' 也能走索引,实际上B+树的有序性只支持前缀匹配,左模糊和全模糊只能全表扫。
另一个坑是过度建二级索引。每个二级索引都是一棵独立B+树,写入时要同步维护,索引越多写放大越严重。建议只针对高频查询条件和联合索引的最左列建模,并用覆盖索引(查询列恰好都在索引里)避免回表。例如下面的联合索引就能让某些查询只走一次树:
-- 建立覆盖索引,避免回表 CREATE INDEX idx_name_age ON user (name, age); -- 该查询只需扫描二级索引叶子节点 SELECT name, age FROM user WHERE name = '张三' AND age > 20;
搞清B+树的节点构成、页分裂以及聚簇和二级索引的差异,就能在表结构设计阶段避开大多数性能陷阱,也能在慢查询排查时准确判断是不是索引没用对。