☰
MySQL索引为什么快?从B+树到磁盘IO原理深度拆解
2026/10/13 8:02:51 网站建设 项目流程

“面试官:MySQL索引为什么这么快?”

两分钟前,候选人还在流畅地讲自己做过的一个订单系统,状态很放松。一听到这个问题,他明显顿了一下,然后开始背:“因为用了B+树,B+树层级少,查询快,叶子节点还有链表,范围查询也快……”

他说得没错。但我觉得,他只是背了一个结论。

我追问了一句:“那为什么同样是树,MySQL不选B树?你的B+树三层大概能存多少数据?”他想了想,说“这个没深入看过”。那一刻我就明白了,他并没有真正理解索引快在哪里。

这篇文章,我打算把“B+树层级少”这句话拆开揉碎,讲清楚底层的数据存取逻辑、一次查询真正经历了什么,以及为什么有时候加了索引反而慢。不管你是准备面试,还是单纯想搞懂索引原理,这篇都值得读完。

1. 面试官这道题想筛掉的,是“背八股”的人

1.1 为什么问“快”而不问“什么是索引”

我做了这么多年技术面试,发现问“索引为什么快”比问“索引是什么”有效得多。后者只要背过定义就能答,前者则逼着你把计算机存储、数据结构、数据库实现几层知识串起来讲。

面试官真正想听的不是“索引是一种数据结构”这种教科书废话,而是希望你从最底层回答:

  • 数据存放在磁盘上,磁盘IO有多慢,慢到什么程度?
  • 索引是如何把“全表扫描”变成“少量磁盘IO定位”的?
  • 为什么偏偏是B+树,而不是二叉树、红黑树、哈希表?
  • 在真实查询中,使用索引后行数从千万降到了几十,这种量级差异是怎么产生的?

这四个问题全部答上来,面试官才会认为你真的用过索引、调优过SQL。只答出“B+树”三个字,在他眼里等于没答。

更关键的是,索引性能原理是后续一切SQL优化的地基。什么覆盖索引、索引下推、最左前缀、回表,所有手段都在围绕“减少磁盘IO”和“避免无谓回表”展开。地基不牢,后面全是空中楼阁。

1.2 “背出结构”和“理解原理”的差距

我经常看到两类回答。

第一类:“索引用的B+树,B+树查询快,所以索引快。”这是标准百度答案,但经不起追问。比如我问你:“那红黑树也是平衡树,查询也稳定在O(logN),为什么MySQL不用?”很多人直接懵了。

第二类:会这样回答:“因为MySQL的数据最终落在磁盘上,磁盘随机IO非常慢,所以核心目标是把查询过程中磁盘IO的次数降下来。B+树每个节点能存上千个key,导致树高很小,千万级数据的树高只有三层,查询一个数据最多读2到3次磁盘页,再加上叶子节点有序,范围查询可以顺序读,于是就有了数量级上的性能提升。”

同样的时间,两个人一开口,高下立判。

第二类回答的背后,是对“数据在磁盘上是按页存的”“节点和页对应”“树高等于磁盘IO次数”这些事实的真实理解。这样的人,即使面试官继续追问“四层B+树能存多少数据”,他也能现场估算出来。

所以说,这道题表面问的是索引,实际筛的是你有没有“从底层原理推导上层现象”的能力。

2. 从磁盘到内存:B+树凭什么成为索引的默认答案

2.1 索引的本质是“少读磁盘”

要想理解索引为什么快,先得回到一个最朴素的事实:数据库的数据存在磁盘上。

磁盘和内存之间的速度差距有多大?一次内存随机访问大概需要几十纳秒到一百纳秒。一次磁盘随机访问呢?机械硬盘大概是10毫秒,固态硬盘快一些,但也要几十微秒到上百微秒。换算下来,一次磁盘随机访问比内存访问慢几个数量级。

MySQL中InnoDB存储引擎的数据读写,以“页”为最小单位。默认情况下,一个页是16KB,数据一行行放在这些页里。当你执行一条没有索引的查询时,MySQL只能把表里所有数据页一个接一个读出来,逐行比对条件。这张表如果有1000万行,占几十万个数据页,哪怕顺序读很快,几十万页读完也要很久。

索引就是为了打破这种“必须把整个表翻完”的困境。它的思路和字典目录很像:你不会从第一页开始翻整本字典找“MySQL”这个单词,而是先看目录页码,直接翻到对应页,再在一页内精确定位。

数据库索引本质上是把这棵“目录树”也存成结构化的页,查询时先读目录树,再读目标数据页。这样,原本需要读几十万页的查询,变成了只读几次目录页加一次数据页。这就是索引快的最底层原因。

2.2 页、节点和B+树之间的映射

既然数据按页存储,索引树要高效,最好一个树节点就恰好占用一个页。这样一来,每读取一个节点,就是一次磁盘IO,而且每次IO拿到的数据都刚好够用。

B+树就是这样的结构。

一个B+树节点里,会顺序存放一组key和一组指针。在InnoDB中,叶子节点会额外存放实际的行数据(聚簇索引)或主键加索引列(二级索引),而非叶子节点只存放key和指向子节点的指针。一个key对应一个数据范围,指针指向下一层的页地址。

当节点里的key数量超过页容量时,节点会分裂成两个,树就往上涨一层。这个设计和页大小密切相关:因为页是固定的16KB,节点容量决定了每个节点最多能存多少key,进而决定了整个树能有多高。

对比一下二叉搜索树:每个节点只能存一个key,两个分支。数据量一千万的时候,树高约log2(10000000),大概是24层。意味着查询一条记录最坏要读24次磁盘,即便不落盘所有非叶子节点,也动不动十几次IO。而B+树一个节点能存上千个key,树高只有三层,IO次数是天壤之别。

2.3 为什么MySQL最终选了B+树而不是B树

很多人最困惑的事情是:B树也有多分支,也矮,为什么不用?

我用一张表把几种常见数据结构的磁盘场景适配对比清楚。

数据结构等值查询范围查询树高/IO次数为什么不适合做数据库索引
哈希表O(1)不支持-无法范围查询,无法排序,哈希冲突难处理
二叉搜索树/红黑树O(logN)需中序遍历约20层以上节点分支少,树高太大,磁盘IO次数过多
B树O(log_1M N)中序遍历,节点间跳转约3-4层非叶子节点也存数据,占用空间,扇出变小;范围查询需要回溯
B+树O(log_1M N)叶子链表顺序扫描约3-4层最适配磁盘场景

这里最关键的两点在于:

第一,B+树的非叶子节点不存数据。非叶子节点只存key和指针,所以一个节点能容纳的key数量更多。同样一个16KB页,如果B树每个节点还要塞一堆用户数据,那能塞的key数量就会大幅缩水,节点分支变少,树自然变高。树高增加,意味着IO次数增加。而B+树把空间最大限度留给key,从根到叶子的路径更短。

第二,B+树的叶子节点用链表串起来了。叶子节点上的key按顺序排列,并且相邻节点通过双向指针连接。执行WHERE age > 20这种范围查询时,只要在树中找到下边界所在的叶子节点,然后顺着叶子链表的顺序一路往后读就行,这些读取是顺序IO,速度非常快。

B树虽然也能范围查询,但它的数据散落在不同层级的节点里,想要取一个范围内的数据,经常要在父子节点之间来回跳转,产生大量随机IO。顺序IO的吞吐能力远高于随机IO,所以B+树在后端数据库这种场景下就成了更优解。

3. 一次索引查询的完整路径:从根节点到叶子节点

3.1 一个三层B+树大概能存多少条记录

要理解B+树“快”,最好亲眼算一次:三层树到底能装多少数据。

为便于估算,我们做一些假设。InnoDB默认页大小是16KB,也就是16384字节。假设表的主键是bigint,在非叶子节点里,每个索引项由“key值”和“指向子节点的页地址指针”组成,key占8字节,指针占6字节,再加些额外开销,我们按14字节算。那么一个非叶子节点最多能容纳大约:

16384 / 14 ≈ 1170 个索引项

也就是说,根节点最多有1170个分支。第二层的每个节点同样最多1170个分支。这样,两颗“非叶子层”最多指向:

1170 × 1170 ≈ 137万个叶子节点

再看叶子节点。在InnoDB的聚簇索引中,叶子节点存放整行数据。假设表中一行数据平均1KB,那么一个16KB的页大约能放16行数据。三层树最多能存放的记录数就是:

137万 × 16 ≈ 2192万

所以你看,在常见的数据规模下,一张几千万行的表,InnoDB的主键索引树往往就是三层。哪怕数据量超过了两千万,四层也完全够用了。

这个估算习惯在面试中非常加分。面试官问你“为什么索引快”,你直接说“三层B+树能存千万数据,所以查询只需要两三次磁盘IO”,比空谈“树高度与数据量对数相关”要实在得多。

3.2 路径上的每一步:从根页加载到页内二分

现在,我们不看理论,跟着一条SQL走一遍。

假设有一张用户表user,以自增主键id为聚簇索引列,我们要执行:

SELECT * FROM user WHERE id = 12345;

MySQL的InnoDB会自动创建以id为索引的聚簇索引树,它的根节点页通常常驻在内存的buffer pool中。查询流程大致是:

  1. 定位根节点页。因为根页常驻内存,这一步几乎不产生磁盘IO。
  2. 在根节点的key数组里用二分查找,找到12345落在哪个子节点的key范围内,得到子节点对应的页号。
  3. 把这个子节点页从磁盘加载到内存。此时发生第一次磁盘IO。
  4. 在子节点页内再次二分查找,找到下一层叶子节点页号。
  5. 加载叶子节点页,发生第二次磁盘IO。
  6. 在叶子节点页内二分查找,定位到目标记录,然后读取行数据。

如果根节点不在内存里,整个查询需要3次磁盘IO;根节点常驻内存时,只要2次。无论数据量是十万还是一千万,这个路径层数几乎不变,这就是稳定的“快”。

你可能会想,页内二分查找难道不耗时吗?当然耗时,但这是内存中的二分,表面上是O(logN),实际上也就几次比较,和动辄10毫秒的磁盘IO相比完全可以忽略。数据库设计者把所有复杂的查找都尽量挪到内存里做,磁盘只负责把整页数据送到内存。

3.3 顺序IO为何让范围查询更快

等值查询看树高,范围查询看叶子链表。

执行WHERE id BETWEEN 10000 AND 10100时,MySQL先在索引树上找到下限10000对应的叶子节点,然后开始沿着叶子链表向右扫描。因为叶子节点本身就是按id有序排列的,而且节点间有指针相连,扫描过程基本就是连续读取一个个页。

这时候,磁盘层面的读取非常接近顺序IO。机械硬盘的顺序读速度可以达到每秒几百MB,而随机IO每一次都要移动磁头,性能低到只剩几MB甚至更低。固态硬盘的随机读好一些,但顺序读依然更快,而且两者的延迟差异同样显著。

反过来,如果B树的叶子节点也存数据,但因为每个叶子的父节点不同,范围查询需要不停地从某个叶子跳到另一个分支的另一棵子树,访问顺序是随机的。数据量一大,随机IO累积的延迟会让查询慢到无法接受。

这也是B+树作为InnoDB默认索引结构的一个隐藏优势:它不只是用树高来压缩等值查询成本,还用有序链表把范围查询变成了“顺序读”表演。

4. “索引这么快”的现实边界:回表、覆盖索引与优化器决策

如果你以为索引在任何情况下都快,那就踩坑了。

真实系统里,加了索引反而变慢的例子比比皆是。面试官特别喜欢在这方面做连环追问,因为理解边界才说明你不是盲目加索引的人。

4.1 回表:从二级索引到主键的隐藏成本

InnoDB的表称为聚簇索引,意思是整行数据都挂在主键索引的叶子节点上。你基于主键查询,一次索引查找就能拿到整行数据。

但我们更多时候是在普通列上建索引,这种索引叫二级索引。二级索引的叶子节点不存整行数据,只存“索引列的值 + 主键值”。当查询需要的字段不在当前索引里时,就必须拿着主键再回聚簇索引树里查一次完整记录,这一步就是“回表”。

比如:

SELECT * FROM user WHERE name = '张伟';

name字段建了普通索引,但SELECT *需要所有列,二级索引的叶子上只有name和id。于是MySQL先查name索引树,找到满足条件的叶子记录(可能很多条),再逐条拿这些id到主键索引树上回表。

如果条件命中了1万行,就要回表1万次。即使每次回表只需要1次磁盘IO,1万次随机IO也足够把查询拖垮。这种情况下,优化器发现全表扫描可能更划算,就会放弃索引。

这就是为什么在面试中不能只背“索引快”,还必须讲清楚回表成本。

4.2 覆盖索引:让查询完全绕开聚簇树

有没有办法不回表?有,最直接的办法就是覆盖索引。

所谓覆盖索引,指的是查询需要读取的所有列,都包含在同一个二级索引中。这样MySQL在二级索引树上就能拿到全部需要的数据,不需要访问聚簇索引。

举个例子。订单表orders有user_id和status字段,经常要按用户查看订单状态:

SELECT user_id, status FROM orders WHERE user_id = 10086;

如果我们只建了idx_user_id这样的单列索引,二级索引叶子节点上是user_id和主键id,不包含status,查询时依然要回表取status。

但如果我们建的是联合索引(user_id, status),这个索引的叶子节点上既有user_id又有status,上面的查询就能直接走索引返回,一次回表都不需要。

当年我参与过一次大查询优化,把SELECT *改成覆盖查询后,一条原本跑200毫秒的SQL降到了20毫秒以下。原因就是去掉了几千次回表IO。

面试中能主动说出“覆盖索引是减少回表的第一手段”,通常会让面试官眼睛一亮。如果还能补一句“MySQL 5.6以后还有索引下推,能在二级索引扫描阶段提前过滤部分数据,进一步减少回表”,那就更完美了。

4.3 优化器放弃索引的常见场景

索引不是万能钥匙。最典型的几个场景,我列在表里。

场景原因解决思路
对索引列使用函数:WHERE YEAR(create_time)=2024函数改变列的值排序,B+树的有序性被破坏,无法二分定位改写为范围条件:WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'
隐式类型转换:WHERE varchar_col = 10086类型转换同样破坏了索引列原始值的有序性统一字段类型,或让常量与列类型匹配
模糊查询:WHERE name LIKE '%abc'前导通配符无法利用有序前缀定位改用全文索引或词条类型,或升级LIKE 'abc%'
联合索引不满足最左前缀B+树对联合索引按最左列优先排序,跳过多列定位自然失效查询尽量带上联合索引的第一列,或按需调整索引列顺序
优化器认为全表扫描更便宜比如命中行数占比过高,扫描大量随机回表成本大于全表顺序读控制结果集规模,或让查询走覆盖索引

这些场景背后有一个统一逻辑:要么破坏了索引列本身的“有序性”,要么破坏了索引与查询之间的“排列一致性”。理解这个逻辑,而不是死记硬背失效条件,遇到具体情况时才能真正做出判断。

面试官只要问一句“为什么给索引列加函数就不走索引”,你从“B+树中key按原值排列,函数把排列顺序打乱,二分查找无从下手”来回答,他立刻知道你懂了原理。

5. 同样一条SQL,explain告诉你它快在哪

5.1 先用type看懂“访问方式”

光会讲原理不够,还得会用工具证明。MySQL里最常见的工具就是EXPLAIN。

执行计划里有一列type,它直接告诉优化器用了什么方式访问数据。从好到差,常见的类型是:

const/system:主键或唯一索引等值查询,最多返回一行,代价极低。

eq_ref:多表连接时,被驱动表通过唯一索引匹配一行。

ref:普通索引等值查询,可能返回多条。

range:索引范围扫描,比如BETWEEN、>、<等。

index:全索引扫描,遍历整棵索引树,比全表扫描略快,但通常也不理想。

ALL:全表扫描,读取所有数据页,性能最差。

面试和实际调优时,最直观的对比就是ALL和ref之间的差距。一个代表读遍全表,一个代表只读一两条记录所在的页。

5.2 建索引前后:一个数量级差异的实例

假设有一张订单表orders,数据量约1000万。我们按user_id查用户订单:

SELECT * FROM orders WHERE user_id = 12345;

建索引前执行计划可能类似:

keytyperowsExtra
NULLALL10000000Using where

这说明MySQL把1000万行全部扫描了一遍。即使每页能放十几行,也至少需要读取几十万个页。即使磁盘按顺序读,几十万个页的IO耗时也是秒级。

建索引后,执行计划变成:

keytyperowsExtra
idx_user_idref37NULL

注意rows从1000万降到了37,这个数字意味着MySQL只读取了少数几个索引页,再根据这37个主键回表。整个查询从秒级降到了毫秒级,这就是索引“快”的直观证明。

在面试中,你完全可以把这样的实战场景讲给面试官听。比起背结论,展示一条SQL在优化前后执行计划的变化,更有说服力。

5.3 面试追问:为什么函数会让索引失效

面试官一旦听到你讲索引失效,大概率会追问原因。

还是那句话:B+树中的所有key都是按原始列的值排好序的。如果把create_time套上YEAR(),那么原本2024-06-01和2024-09-01在函数作用下都变成2024,原有的相对顺序被破坏。优化器无法在这棵“顺序已乱”的树里做二分定位,于是只能放弃索引。

同样,字符串列和数字比较时,MySQL可能先对字符串隐式转成数字,再拿转换后的值和数字比,索引树里存的还是原始字符串,排序方式对转换后的值完全无效。

这类问题的答案,归根结底都在“有序性”三个字上。

6. 我建议的回答套路:三步把这道题答到点上

如果你下周就要面试,我建议你按下面三步来组织回答,既不会偏题,也不会显得像背书。

第一步,说底层目标。索引是为了减少磁盘IO,因为磁盘IO是数据库最大的性能瓶颈。

第二步,说数据结构。用B+树是因为它在磁盘场景下树高很低,比如三层可以支撑千万级数据,所以查询只需要两三次磁盘IO。叶子节点有序且用链表连接,范围查询可以顺序读。

第三步,说工程边界。索引不是无脑快,要关注回表、覆盖索引、最左前缀和优化器选择。比如SELECT *可能带来回表,覆盖索引可以避免回表。

你可以试着组织一段完整回答:

“在InnoDB中,数据按16KB的页存放在磁盘上,磁盘随机IO比内存慢几个数量级,所以索引的核心目标是把查找次数压缩到几次。B+树中每个节点对应一个页,内部节点只存key,所以一个16KB的页可以容纳上千个key,一千万行数据的树高度只有三层。这意味着一次等值查询只需要两三次磁盘IO。范围查询时,叶子节点通过链表相连,可以顺序访问。同时,二级索引可能面临回表问题,所以实际使用中还要考虑覆盖索引、联合索引最左前缀等策略,才能真正发挥索引的性能。”

这段回答能在90秒内讲完,既有深度又有广度。面试官听完不会觉得你在背八股,反而会觉得你真的在线上环境里处理过慢查询。

最后再分享一个我自己的感受:一道“索引为什么快”的题,本质是检验你有没有把“磁盘IO、页、树、索引”这四件事串起来。我见过很多面试者把B+树背得滚瓜烂熟,但连页大小都没听说过。其实只要自己动手建一张千万行的表,跑几次EXPLAIN,观察rows的变化,比背十遍答案都管用。下次再有人问你索引为什么快,你可以不慌不忙地从磁盘讲起。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询