面试总问的B+树,我把磁盘IO到底怎么算的讲清楚了

简介: 从磁盘IO的底层约束讲起,逐层对比哈希、二叉、红黑树、B树与B+树,讲清MySQL为什么选B+树(矮胖树、顺序IO、范围查询、查询稳定),并用这套底层理解反过来指导覆盖索引、前缀索引、最左前缀等日常索引设计。

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

“为什么MySQL的索引用B+树,不用哈希,不用红黑树?”很多DBA面试都会被问到这个问题。你的答案是"B+树矮胖、叶子节点有链表、支持范围查询"。面试官如果接着问:那为什么矮胖就好?你该怎么答?

我在这方面也被问住过,后来自己啃了几天存储引擎的源码和文档,才把这层窗户纸捅破。今天不背八股,从磁盘IO这个最底层的约束,一步步推,为什么最后是B+树。推完你会发现,后面那些索引设计的原则,全是这个原理长出来的。

一、一切的起点:磁盘IO很贵

数据库的瓶颈,绝大多数不在CPU,在磁盘IO。数据存在磁盘上,读一次要等磁头转过去。这个等待,比内存慢好几个数量级。

索引的价值,就是少读几次磁盘。你查一行数据,如果不走索引,可能要把整张表扫一遍,读几千个数据块。走索引,读几个块就定位到了。所以评价一个索引结构,核心指标就一个:一次查询,最少读几次磁盘。

这个"读几次磁盘",在磁盘术语里叫IO次数。数据库每次IO,读的是一页,不是一行。页是存储的最小单位,默认16KB。所以问题的本质变成:给定一个数据结构,查一条数据,要走多少次页。

二、为什么不用哈希

先看哈希。哈希索引查单点,确实快,一次哈希定位,接近O(1)。但它有个致命短板:只能查等值。

你写WHERE id = 5,哈希索引飞快。但写WHERE id > 5或者ORDER BY id,哈希直接废了。哈希是散列的,数据排不排序它不管,范围查询只能全表扫。

还有一个问题,哈希冲突。数据多了,冲突多了,性能就退化。所以哈希索引适合精确匹配,不适合当通用的主索引。

三、为什么不用二叉和红黑树

再看树。二叉搜索树,理论上查找是O(log n)。但它有个问题,树的高度和数据的插入顺序强相关。数据有序插入,二叉搜索树会退化成链表,高度变成n,查询变成O(n),等于全表扫。

红黑树解决了退化问题,它通过旋转保持平衡,高度稳定在log n。但注意,红黑树是"内存里的平衡树"。它的节点比较小,一个节点就一个数据。放到磁盘上,一个页16KB,如果只装一个数据,太浪费了。

更要命的是,红黑树高度对数据库来说还是太高。数据量一大,比如千万行,log2(1000万)大概是24。也就是说,查一次要走24层,24次磁盘IO。这太慢了。

四、B树:矮了,但还不够

B树就是来降低高度的。它的核心思想:一个节点多存几个数据,把树压矮。每个节点存一批键,扇出高,树就矮了。千万行数据,B树可能只要3到4层。

但B树有个特点:数据分散在所有的节点上,不光叶子节点。查一个数据,可能在根节点就命中了,也可能在中间的节点。这就带来一个问题:查询性能不稳定。同一张表,有的查询快,有的查询慢,看数据落在哪一层。

而且B树的叶子节点之间没有链表相连。要做范围查询,比如取id在100到200之间的所有数据,得从根节点开始,沿着树结构找到100所在的节点,再一层层回溯到父节点、找到200的位置,来回走好几趟。每走一层都是一次随机IO,效率远不如B+树顺着叶子节点链表一路扫过去。

五、B+树:为什么它赢了

B+树是B树的改良,改了两处,正好补上B树的短板。

第一处,数据全放在叶子节点。非叶子节点只存键,不存数据,用来当"目录"。这样每个非叶子节点能塞更多的键,扇出更高,树更矮。在MySQL默认16KB页大小和典型配置下,千万行的表,B+树通常3层就够了。3层,就是3次磁盘IO。

第1层(根): [10, 20, 30]                 ← 只存键,当目录
第2层:      [1..10] [11..20] [21..30]      ← 还是目录
第3层(叶子): [数据][数据]...[数据][数据]   ← 真正的数据全在这
              ↑ 叶子之间用链表串起来

第二处,叶子节点用双向链表串起来。这直接解决了范围查询,从前向后扫一遍就行,不用在树里来回跳。要取id在100到200之间的数据,找到100所在的叶子,顺着链表往后扫就行。一次顺序IO,把整段数据读出来。顺序IO比随机IO快得多。

这两个改动,让B+树同时拿到了两个优势:树矮,IO次数少且稳定。支持范围查询,走顺序IO。所以它成了关系型数据库的主流索引结构。

六、这套原理,怎么指导日常设计

理解了底层,再回头看索引设计,很多"规则"就自动想通了。

先说覆盖索引。既然数据全在叶子节点,那如果查询要的列,叶子节点里都有,就不用回表。这就是覆盖索引。EXPLAIN里Extra显示Using index,就是命中覆盖索引。设计时,把高频查询要的列一起建进索引,能省一次回表IO。

-- 联合索引 (user_id, status),查询只要这两列 → 覆盖索引,不回表
EXPLAIN SELECT user_id, status FROM orders
WHERE user_id = 12345;
-- Extra: Using index

再说最左前缀。联合索引(a, b, c),数据按a排,a相同再按b排。所以查询必须从a开始,跳过a直接用b,索引就用不上。这不是规则,是B+树按序存储的必然结果。理解了结构,就不会再问"为什么最左前缀"。

还有前缀索引。大字段(比如长文本)建索引,整列建会很大,占空间。可以只取前N个字符建索引。但要注意,前缀索引不能做覆盖索引优化。这个取舍,也是从存储结构推出来的。

避坑清单

别给所有列都建索引。每个索引都是一棵B+树,写入时都要维护。索引越多,写入越慢,空间越大。我见过一张表建了十几个索引,写请求全堵在索引维护上。索引是给查询用的,写多的表,索引要克制。

联合索引别乱建,先想最左前缀。很多人把要用的列一股脑塞进联合索引,结果查询跳过第一列,索引白建。建之前,列一下实际查询的WHERE条件,按条件顺序建,比瞎建强。

别一听"覆盖索引好"就到处用。覆盖索引要包含所有查询列,列一多,索引就大,反而得不偿失。我的原则:只给高频查询、返回列少的场景用覆盖索引。低频的、SELECT *的,老实回表。

我的判断

B+树这套原理,看着像面试八股,其实每天都在用。你写的每条SQL,优化器都在背后做着"走哪棵B+树、读几次磁盘"的决策。懂原理的人,索引设计靠推导。不懂的人,只能靠猜和试。

这也是我为什么建议别背结论。哈希为什么不行,红黑树为什么不行,B+树为什么行,自己推一遍,比背十遍管用。

下次再有人问你"为什么用B+树",你不用背,从磁盘IO讲起就行。


你面试被B+树问倒过吗?索引设计翻过车没?评论区聊聊。我猜很多人背过"B+树矮胖"这句话,但没想过它到底为什么赢。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关文章
|
1月前
|
SQL 关系型数据库 MySQL
死锁报错看了三遍没看懂?我拆给你看(附定位SQL)
从一次真实死锁现场切入,讲清行锁、间隙锁、插入意向锁的加锁机制与死锁形成原理,手把手教你怎么用show engine innodb status和information_schema定位死锁,并给出加锁顺序设计等避坑清单。
|
1月前
|
存储 SQL 容灾
共享存储集群 vs 分布式多副本:同城双活两条技术路线怎么选?
同城双活正在成为金融、政务等核心系统的容灾标配——RPO=0、RTO<30秒。但真正的落地远不止“两个机房各放一套数据库”那么简单。网络延迟的容忍度、脑裂预防机制、同步复制的性能代价、以及故障切换后的数据回滚,每一个环节都可能成为“最后一公里”的绊脚石。本文从容灾架构演进入手,拆解同城双活的核心技术原理、关键挑战与应对方案,并结合同城双中心方案及实测数据进行深度解析。
|
1月前
|
SQL 人工智能 运维
3个月AI Agent运维实测:慢SQL它管,根因还得我上
以三个月实测的视角,划清AI Agent自治运维的真实能力边界:巡检、慢SQL发现等重复活已可替代,复杂根因、变更审批、数据兜底仍需人把关,探讨DBA角色从救火队员向定规则、把关人的转型。
|
1月前
|
SQL 存储 关系型数据库
分区裁剪失效、DDL卡死、元数据爆炸:分区表的3个真实代价
很多人觉得分区表是“轻量级分库分表”——数据分开放、查询只扫一个区、过期数据直接DROP分区,听起来很完美。但分区表有严格的适用边界和隐藏代价:分区键选错导致所有查询都扫全部分区、分区数量过多导致DDL巨慢、跨分区查询比普通表还慢……本文从分区表的核心原理出发,拆解4种分区类型、3个真实踩坑场景,以及分区表与分库分表的本质区别,帮你一次性搞清楚到底该不该用。
|
1月前
|
SQL 运维 关系型数据库
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
分布式数据库选型,99%的文章在列表格比参数——但真正的决策关键不在厂商PPT里,在上线后的真实运维里。本文从网络延迟容忍度、SQL兼容性验证、在线扩容能力、全局索引代价、运维工具链成熟度5个维度出发,给出可落地的评估方法和决策建议,帮助你在选型阶段避开那些“只有上线后才会发现”的坑。
|
1月前
|
JSON 关系型数据库 MySQL
MySQL 5.7升级到8.0之后,JSON查询的性能瓶颈真的解决了吗?
MySQL 5.7引入原生JSON类型,8.0支持多值索引,至今已近十年。但大量开发人员仍在把JSON当“万能兜底字段”——不管什么数据都往里塞,等查询慢到怀疑人生才想起来排查。本文拆解JSON字段查询的5个高频踩坑场景,给出虚拟列索引、多值索引等正确的优化方案。
|
27天前
|
SQL 运维 算法
订单表上亿行,我按用户ID拆成128片之后怎么样了
从单表几千万行慢查询的痛点出发,讲清垂直拆分与水平拆分的区别、分片键怎么选、分片算法(hash取模/range/一致性hash)怎么权衡,以及分库分表带来的分布式ID、跨片查询、分布式事务等问题,给出避免过度拆分的避坑清单。
|
26天前
|
SQL Java 数据库连接
1万行插入13秒到0.9秒:ORM批量插入只差一个参数
从一次列表接口慢的排障讲起,发现2000多条一模一样的N+1查询。文章拆开ORM生成慢SQL的三类典型病:N+1懒加载、逐条批量插入(只差一个rewriteBatchedStatements参数)、隐式转换让索引白建。给出JOIN/批量IN/@BatchSize的取舍、MyBatis与JPA各自的修法,以及用performance_schema按SQL指纹抓N+1、测试环境打印真实SQL的协作办法。
|
29天前
|
SQL 关系型数据库 MySQL
别再盯着EXPLAIN的rows列了,8.0.18之后有更好的选择
EXPLAIN是DBA最常用的工具之一,但大多数人还在看type、rows、Extra这些传统字段——然后靠经验猜。MySQL 8.0.18开始引入的EXPLAIN ANALYZE,直接把实际执行时间和行数输出给你看,不用猜了。本文对比传统EXPLAIN和EXPLAIN ANALYZE的差异,展示如何用新工具把执行计划分析这件事从“猜”变成“看”。
|
21天前
|
存储 关系型数据库 MySQL
对账差了三毛钱,查完我把全部金额字段从DOUBLE改成了DECIMAL
一次财务对账差三毛钱的排查,牵出金额字段用浮点数的老坑。从IEEE 754为什么存不准0.1讲起,用同一批金额把FLOAT、DOUBLE、DECIMAL三种类型实测对比,再给出金额字段的选型、聚合与改表做法,附避坑清单。