故事场景:查新华字典找一个生僻字
想象你要在一本 1500 页的新华字典里查一个生僻字。两种查法:
- 一页一页往后翻——这叫全表扫描,找到天黑也未必找得到;
- 先翻前面的”部首检字表”,查到这个字在第 888 页,直接翻过去——这张检字表,就是索引。
MySQL 的索引,就是给数据表配的一张”检字表”。那为什么这张表偏偏要用 B+ 树来组织?咱们从”翻书的次数”说起——在数据库里,每多翻一次书,就是多一次磁盘 IO,而 IO 是慢的代名词。
第一候选:二叉树目录——层数太深,翻到手酸
如果检字表按二叉树组织:每个节点只存一条记录,分两个叉。一本百万级数据的字典,树高 20 多层,查一个字要翻 20 多次书。
问题不在查找算法,在树太高。磁盘 IO 一次就是毫秒级,20 次叠加起来,慢得肉眼可见。结论:索引结构的第一要务,是把树压矮。
第二候选:B 树——目录页里夹了太多正文
想把树压矮,就让每个节点多分几个叉——这就是 B 树:一个节点存很多条记录,一个节点正好对应磁盘的一页,读一页就能排除一大片。
但 B 树有个毛病:每个节点里既放路标(键),又放正文(数据)。正文占地方,一页能塞下的路标就变少,叉变少,树又悄悄长高了。而且数据散落在每一层,范围查询(比如查所有”氵”开头的字)得在树里上蹿下跳,没法顺着一条线扫过去。
最终赢家:B+ 树——目录只放路标,正文全在最后一层
B+ 树做了两个关键改动,一下子把问题全解决了:
- 非叶子节点只存路标,不存正文。 同样是一页磁盘,能塞下多得多的路标,分叉成百上千。结果就是:3 到 4 层,就能管住千万级甚至上亿的数据,查任何一条记录都只要 3、4 次 IO。
- 正文全在叶子层,而且叶子之间手拉手串成一条链表。 查范围的时候,先定位到起点,然后顺着链表一路扫下去就行——这就是为什么 B+ 树对
BETWEEN、范围排序这么友好。
顺带一提:因为所有数据都在叶子层,每次查询的 IO 次数稳定,不会出现”运气好在第二层就撞到”的忽快忽慢,性能可预期——这对数据库来说非常重要。
顺藤摸瓜的三个高频面试点
1. 聚簇索引与回表 InnoDB 里,主键索引就是聚簇索引:它的叶子节点直接存整行数据——等于正文就印在检字表的最后一级,按主键查,一次到位。 而普通索引(二级索引)的叶子只存主键值:先在二级索引查到主键,再回主键索引查整行,这个动作叫回表。多走一趟,就多一份成本。
2. 覆盖索引
如果你要查的列,恰好在二级索引里全都有(比如索引是 (name, age),你只查 name 和 age),那查到索引就齐活了,根本不用回表——好比检字表上直接印了拼音和解释,连正文页都不用翻。优化慢查询时,”把查询改成覆盖索引”是最常见的招数之一。
3. 最左前缀原则
联合索引 (姓, 名) 的排序规则是:先按姓排、同姓再按名排,跟电话簿一模一样。所以:只报姓,能查;只报名,查不了——目录不是按名编的。这就是最左前缀:联合索引从最左列开始才用得上,中间不能断。
和表设计的关系
索引是”查”的学问,表结构是”存”的学问,两件事是一对搭档。存的规矩,我在《关系型数据库设计三大范式到底是什么?》里讲过:先按三范式把表设计干净,再根据查询场景加索引,别反过来——表设计一团乱麻,再好的索引也救不回来。
简单粗暴地说: MySQL 选 B+ 树,就一句话——树最矮、IO 最少、范围查最顺。二叉树输在高,B 树输在目录页被正文挤占,B+ 树把路标和正文彻底分家,赢得干净利落。
怎么样,老铁!下次面试官问”为什么是 B+ 树”,你就把这本新华字典递过去:从翻书次数讲起,顺手把回表、覆盖索引、最左前缀一串带出来,这题就是送分题。
动手验证:上面这些结论,用
EXPLAIN全都能亲眼看到。想练手的话,拿台小云服务器装个 MySQL 慢慢造数据最方便,选购可以看我整理的云服务器选购指南,新客首单很便宜,别买贵了。