想象你第一次走进一座巨型图书馆。门口有一块“楼层导航屏”(根节点),只写着“文学类在 2 楼,理工类在 5 楼”;到了 2 楼,走廊里的“分类指示牌”(内部节点)继续告诉你“历史书在东侧,小说在西侧”;直到你走到最深处的书架前,才看到一本本真正的书(数据)整齐排列。最妙的是,同一排书架之间有小门连通,你可以在那一排从头走到尾,把某个分类的所有书全部扫一遍,不用反复回大厅。 这就是 B+ 树——数据库索引世界里的“图书馆导航系统”。它不只是一棵“更粗的二叉树”,而是一套为磁盘而生的多路平衡结构:用高扇出压低树高、用叶子链表加速范围扫描、用“非叶不存数据”保证查询稳定。今天我们就沿着这座图书馆的动线,把 B+ 树的每一层都走一遍。

岗位关联

理解 B+ 树是数据库索引的地基。无论是排查慢 SQL、设计复合索引,还是读懂 EXPLAIN 中的 key 与 rows,你的每一步实战,都在和这棵树的层数、扇出、叶子链表打交道。

目录
  1. 可视化演示
  2. 为什么需要B+树
  3. 多路平衡结构
  4. 叶子链表组织
  5. 分裂与合并
  6. 高扇出低树高
  7. B树对比B+树
  8. InnoDB工程实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

为什么需要B+树

在图书馆里,如果每层只设“两块指示牌”(二叉树),那么你要找一本书,就得一层一层往下走,楼层数随藏书量翻倍式增长。一百万本书的图书馆,从大门走到最底层书架竟然要上下 20 层——这在磁盘世界里是不可接受的,因为每走一层就要读一次磁盘页,而一次磁盘 IO 的时间足够 CPU 执行几十万条指令。

所以问题来了:能不能让一块“指示牌”同时指向更多方向?答案就是“多路”(M 路)结构——一个节点不再只有两个孩子,而是可以有几百上千个孩子。孩子多了,树的层数自然就矮了;树矮了,从根到叶子的“走动次数”就少了,IO 也就少了。这就是 B+ 树诞生的出发点:为磁盘而生的多路平衡查找树。

更确切地说,二叉查找树的高度与数据量的关系是:

回到图书馆:让一块指示牌指向 20 个方向,楼层数就从 20 层骤降到 2-3 层。读者每次找书少走十几层楼,数据库每次查询少读十几个页——这就是 B+ 树的所有“生意经”。

hBST≈⌈log⁡2(N+1)⌉h_{\mathrm{BST}} \approx \left\lceil \log_{2}(N+1) \right\rceil
数据量 N 每翻一倍,二叉树高度大约加 1;当 N=100 万时,树高约 20 层,即最坏要 20 次磁盘 IO。
  1. 向上取整树的高度必须是整数层,所以用 $\left\lceil \cdot \right\rceil$ 表示“至少需要多少层”。
    ⌈⋅⌉\left\lceil \cdot \right\rceil
  2. 以 2 为底的对数二叉树每层最多容纳两倍于上一层的节点,所以高度随节点数呈 $\log_{2}$ 增长。
    log⁡2(N+1)\log_{2}(N+1)
  3. 代入实际规模当 $N = 1{,}000{,}000$ 时,$\log_{2}(1{,}000{,}001) \approx 20$,这就是 20 次 IO 的来源。
    log⁡2(1,000,001)≈20\log_{2}(1{,}000{,}001) \approx 20

多路平衡结构

现在我们来到图书馆的核心设计:一块“分类指示牌”到底能指多少方向?在 B+ 树里,这个数字用“阶数” $M$ 表示。一棵 $M$ 阶 B+ 树,每个节点最多有 $M$ 个孩子;而键值就像指示牌上的分类标签,数量永远比孩子数少 1。假设你在 2 楼看到一块牌子写着“历史 001-100、地理 101-200、科学 201-300”,它就能带你去往 4 条不同的走廊(小于 001、001-100、101-200、大于 200)。

为了保持“平衡”,节点不是想空就空、想满就满:每个内部节点的键数有个上下界。太少会浪费层级,太多会撑爆节点。这个约束保证整棵树永远是“匀称”的,不会退化成一条链表。

回到图书馆:每块指示牌都有“最少指向数”和“最多指向数”,这样所有楼层的规模都差不多,不会出现某一层指示牌多到爆、另一层空空如也的畸形结构。这个约束正是 B+ 树常年保持“平衡”的秘密。

⌈M2⌉−1≤nkeys≤M−1\left\lceil \frac{M}{2} \right\rceil - 1 \le n_{\text{keys}} \le M - 1
一个 $M$ 阶 B+ 树的内部节点,键数至少是 $\lceil M/2 \rceil - 1$ 个,至多 $M-1$ 个;对应的孩子数则至少 $\lceil M/2 \rceil$ 个、至多 $M$ 个。
  1. 最大键数 $M-1$$M$ 个孩子之间的分隔线至多有 $M-1$ 条,即键值个数。
    nkeys≤M−1n_{\text{keys}} \le M - 1
  2. 最小键数为了防止节点过空导致树失衡,规定至少存 $\lceil M/2 \rceil - 1$ 个键。
    nkeys≥⌈M2⌉−1n_{\text{keys}} \ge \left\lceil \frac{M}{2} \right\rceil - 1
  3. 孩子数与键数差一每个键对应一个孩子区间,所以孩子数 $= n_{\text{keys}} + 1$。
    nchildren=nkeys+1n_{\text{children}} = n_{\text{keys}} + 1

叶子链表组织

如果你在图书馆里找所有“编号 300-399 的历史书”,最笨的办法是每一层都翻一遍。但 B+ 树的聪明之处在于:只有最底层的书架(叶子节点)才真正存放书籍,并且这些书架沿着走廊一字排开,相互之间有小门连通。你只需要一路走到起点书架,然后顺着通道从头扫到尾,一本书都不会漏。

这对应 B+ 树中最重要的结构特征:所有键值数据都存储在叶子节点,叶子之间用指针串成一个有序链表。内部节点只扮演“路牌”的角色,告诉你该拐向哪个方向。范围查询时,先沿树向下找到第一个目标叶子(花费树高 $h$ 次 IO),再沿叶子链表顺序扫描,不需要回头游走。

图书馆里,这条连通各书架的走廊,就是 B+ 树的叶子链表。它让“给我所有价格在 500-800 元的商品”这类查询,不再是灾难性的全表扫描,而是一次潇洒的“顺藤摸瓜”。

IOrange≈h+kIO_{\text{range}} \approx h + k
$h$ 是从根到第一个叶子的层数,$k$ 是本次范围查询涉及的叶子页数。范围查询的成本,约等于“下楼梯”加“走廊里扫一遍”。
  1. 树高 $h$从根节点走到第一个命中的叶子,需要读 $h$ 个页。
    h=树高h = \text{树高}
  2. 命中叶子数 $k$顺着叶子链表向后扫描,每多一个区间就多读一个叶子页。
    k=命中的叶子页数k = \text{命中的叶子页数}
  3. 总 IO 近似和范围查询不再重复走根到叶子的完整路径,总 IO 约为两者之和。
    IOrange≈h+kIO_{\text{range}} \approx h + k

分裂与合并

图书馆的书架再大,藏书也会满。当某个书架的隔间被塞满,管理员会在旁边架起一个新书架,把原来的书对半分成两堆,并在上层的分类指示牌上增加一个新的分类号。这就是 B+ 树的“节点分裂”。反过来,如果一本书架的书被借走太多、剩下不到一半,管理员会把几个书架合并成一个,让空间不再浪费。

分裂是 B+ 树写入路径上最核心的动作:当一个节点插入后键数超过 $M-1$,就从中间位置一分为二,把中间键上提给父节点;如果父节点也因此满了,就继续向上分裂,直到根节点。当根节点也分裂时,树的层数加一。删除则相反:键数低于下界时与兄弟节点合并或借键,保证树一直满足“半满以上”的约束。

管理员的动作看似繁琐,但它保证了一件事:无论插入多少本书,图书馆的楼层都只会在必要时增加一层,绝不会像二叉树那样因为插入顺序不当而变成一条歪歪扭扭的长廊。B+ 树的平衡,就是这样一次次“拆书架、换路牌”换来的。

split_pos=⌈M2⌉\text{split\_pos} = \left\lceil \frac{M}{2} \right\rceil
分裂位置大约取节点键数的中间值,左半留给原节点,右半给新节点,中间键上提到父节点继续“排队”。
  1. 分裂位置满节点有 $M-1$ 个键,分裂点取 $\lceil M/2 \rceil$,让左右尽量均衡。
    split_pos=⌈M2⌉\text{split\_pos} = \left\lceil \frac{M}{2} \right\rceil
  2. 中间键上提把分隔左右的关键键值复制/上移到父节点,作为新的路牌。
    middle_key=keys[split_pos]\text{middle\_key} = \text{keys}[\text{split\_pos}]
  3. 递归传播父节点插入中间键后若再次溢出,则继续向上分裂,直到根。
    while overflow: split(node), lift(key)\text{while overflow: split(node), lift(key)}

高扇出低树高

现在我们来算一笔“楼层账”。B+ 树之所以能只建 2-3 层楼,全靠“高扇出”。扇出(Fanout)就是每个节点能有多少个孩子。二叉树的扇出固定是 2,而 B+ 树的一个节点可以轻易拥有几百上千个孩子——因为它的内部节点只存键和指针,不存数据,一个 16KB 的页能塞下几百个“分类号”。

数据量 $N$ 与树高 $h$ 的关系是:

图书馆里,1000 层的摩天大楼和 4 层的平房,找书效率天差地别。数据库里每“下一层楼”就是一次磁盘 IO,所以树矮 1 层,意味着每次查询少读一个页。高扇出不是锦上添花,而是 B+ 树压倒二叉树的决定性优势。

h≈⌈log⁡MN⌉+1h \approx \left\lceil \log_{M} N \right\rceil + 1
$M$ 是扇出(每个节点的孩子数),$N$ 是数据总量。扇出越大,$N$ 被“分成几份”的次数越少,树就越矮。
  1. 取对数每向下一层,能覆盖的数据量乘以 $M$,所以层数与 $\log_{M} N$ 成正比。
    log⁡MN\log_{M} N
  2. 加 1 得到叶子层内部节点走完还要到叶子层取数据,所以总高度在内部层基础上加 1。
    ⌈log⁡MN⌉+1\left\lceil \log_{M} N \right\rceil + 1
  3. 代入具体数字当 $M=1000$、$N=10$ 亿时,$\log_{1000}10^{9}=3$,加上叶子层共 4 层。
    log⁡1000109=3\log_{1000}10^{9} = 3

B树对比B+树

B+ 树的名字里有个"+",那它和原版 B 树到底差在哪?最核心的一点是:B 树把数据放在所有节点里(就像每层楼都塞几个书架),而 B+ 树的非叶子节点只放路牌、真正数据全部沉到叶子层。这个差异带来三个连锁优势。

第一,稳定性。B 树里有些键在高层就能命中,有些要走到底层,查询时间忽快忽慢;B+ 树所有查询都要走完从根到叶子的完整路径,时间恒定。第二,空间利用率。非叶节点不存数据,同样的页能放更多键,扇出更大、树更矮。第三,范围查询。B 树的节点分散在不同层,要中序遍历才能扫出区间;B+ 树的叶子有序链成一条线,直接顺序扫描即可。

这就是为什么 B+ 树能“一棵树打天下”:它牺牲了中间层“提前命中”的小概率甜头,换来了更矮的树、更稳的延迟和更顺滑的范围扫描。对数据库而言,这三个价值远比“偶尔少走一层楼”重要。

节点承载的键数≈页大小键长+指针长\text{节点承载的键数} \approx \frac{\text{页大小}}{\text{键长} + \text{指针长}}
非叶节点存的越少,单位空间能装的键越多。B 树每个节点还要扛数据行,B+ 树只扛“路牌”,所以扇出高出一两个数量级。
  1. 页大小数据库一次 IO 读入一个固定大小的页(如 16KB),这是节点的物理容器。
    page size=16KB\text{page size} = 16\text{KB}
  2. 键长加指针长每个索引项占用的空间约等于“键值 + 指向子节点的指针”。
    entry size=key+pointer\text{entry size} = \text{key} + \text{pointer}
  3. 相除得扇出两者相除,就是一个节点最多能放多少个索引项。
    fanout≈16KB8B+6B≈1000\text{fanout} \approx \frac{16\text{KB}}{8\text{B}+6\text{B}} \approx 1000

InnoDB工程实践

MySQL 的 InnoDB 引擎是 B+ 树在工业界的头号代言人。在 InnoDB 里,一棵 B+ 树的每个节点就是一个 16KB 的数据页。表的主键索引(聚簇索引)的叶子页直接存放整行数据;二级索引(非聚簇索引)的叶子页则存放“索引键 + 主键”。当查询用二级索引时,先在小树上找到主键,再回聚簇索引取整行数据,这个过程叫“回表”。

InnoDB 的经典估值公式是:

回到 InnoDB 的世界里,这棵三层小楼就是一张千万级订单表的“户口本”。日常的 WHERE id = 123 是点查,走上 3 步;WHERE create_time BETWEEN ... 是范围查,走 3 步再顺着叶子链表扫。理解这棵小楼的每一层,你就能解释为什么索引设计得好不好,直接决定一条 SQL 是 3 次 IO 还是全表 10 万次 IO。

MaxRows三层≈⌊16KB8B+6B⌋2×16KBrow size\text{MaxRows}_{\text{三层}} \approx \left\lfloor \frac{16\text{KB}}{8\text{B}+6\text{B}} \right\rfloor^2 \times \frac{16\text{KB}}{\text{row size}}
若主键用 8 字节的 BIGINT,内部指针 6 字节,则每页约 1000 个键;两层索引约 100 万个指向叶子页的指针;每个叶子页按 1KB 数据行约存 16 行,因此 3 层的树可支撑约 1600 万行数据。
  1. 页大小替换把页大小 16KB 代入公式,得到每页索引项数。
    16KB8B+6B≈1000\frac{16\text{KB}}{8\text{B}+6\text{B}} \approx 1000
  2. 两层索引根节点 + 中间层,能指向大约 $1000 \times 1000$ 个叶子页。
    10002=1,000,0001000^2 = 1{,}000{,}000
  3. 乘以每页行数每个叶子页按行大小能放若干行,1KB/行可放 16 行。
    1,000,000×16≈1600 万1{,}000{,}000 \times 16 \approx 1600\text{ 万}

AI 应用场景

慢 SQL 优化排查

阶段:模型特征服务数据链路调优

解决的问题:训练和特征服务中频繁按用户 ID / 时间戳拉取数据,传统全表扫描单次查询动辄几百毫秒到数秒,拖慢整个特征流水线。理解 B+ 树后,DBA 通过 EXPLAIN 检查 key、rows、type,确认查询是否命中聚簇索引叶子链表,将扫描范围从全表收敛到几个叶子页。

具体案例:某风控模型的特征表有 8000 万行,按 user_id 点查特征。建立主键索引后,查询从 1.8 秒降到 8 毫秒——正是 B+ 树三层结构把 IO 从几十万次压到 3 次的结果。

电商订单范围分页

阶段:业务数据查询与分析

解决的问题:订单表 WHERE order_time BETWEEN ? AND ? ORDER BY order_time 这类范围查询,如果表上只有主键索引,就会全表扫描。利用 B+ 树叶子链表的有序性,order_time 二级索引可以让查询只扫命中时间段内的叶子页,而不是全表。

具体案例:某电商后台按“近 30 天订单”导出报表,未加时间索引时需要 40 秒;加上 (order_time) 索引后,走 B+ 树叶子链表顺序扫描,耗时降到 0.3 秒。

索引设计取舍

阶段:系统架构与存储设计

解决的问题:每个索引都是一棵独立的 B+ 树,写入时都要维护,索引越多写放大越严重。架构师需要根据读写比例决定建哪些索引——读多写少的报表系统可以多建索引,写多读少的日志系统要克制。

具体案例:某 AI 平台埋点日志表每秒写入上万条,最初建了 6 个索引,批量写入常被节点分裂拖慢;砍到 2 个核心索引后,写入吞吐提升 3 倍,查询仍能命中 B+ 树叶子链表。

Python 演示

python97 行
import bisect  class BPlusNode:    """B+ 树节点:叶子节点存数据,内部节点只存索引键。"""     def __init__(self, is_leaf=True):        self.keys = []        self.children = []        self.is_leaf = is_leaf        self.next_leaf = None  class BPlusTree:    """教学用 B+ 树:支持插入、查找、树高展示。"""     def __init__(self, order=4):        self.order = order        self.max_keys = order - 1        self.root = BPlusNode()     def _find_leaf(self, node, key):        while not node.is_leaf:            i = bisect.bisect_right(node.keys, key)            node = node.children[i]        return node     def _insert(self, node, key):        if node.is_leaf:            pos = bisect.bisect_left(node.keys, key)            node.keys.insert(pos, key)            if len(node.keys) <= self.max_keys:                return None            mid = self.order // 2            right = BPlusNode(is_leaf=True)            right.keys = node.keys[mid:]            right.next_leaf = node.next_leaf            node.keys = node.keys[:mid]            node.next_leaf = right            return right.keys[0], right        i = bisect.bisect_right(node.keys, key)        res = self._insert(node.children[i], key)        if res is None:            return None        up_key, right = res        node.keys.insert(i, up_key)        node.children.insert(i + 1, right)        if len(node.keys) <= self.max_keys:            return None        mid = self.max_keys // 2        up_key = node.keys[mid]        right = BPlusNode(is_leaf=False)        right.keys = node.keys[mid + 1:]        right.children = node.children[mid + 1:]        node.keys = node.keys[:mid]        node.children = node.children[:mid + 1]        return up_key, right     def insert(self, key):        res = self._insert(self.root, key)        if res is not None:            up_key, right = res            new_root = BPlusNode(is_leaf=False)            new_root.keys = [up_key]            new_root.children = [self.root, right]            self.root = new_root     def search(self, key):        leaf = self._find_leaf(self.root, key)        return key in leaf.keys     def height(self):        node = self.root        h = 1        while not node.is_leaf:            h += 1            node = node.children[0]        return h     def show(self, node=None, depth=0):        if node is None:            node = self.root        tag = "leaf" if node.is_leaf else "int "        print("  " * depth + tag + " " + str(node.keys))        if not node.is_leaf:            for child in node.children:                self.show(child, depth + 1)  if __name__ == "__main__":    tree = BPlusTree(order=4)    for key in [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]:        tree.insert(key)        print("插入 %3d -> 树高=%d, 根 keys=%s" % (key, tree.height(), tree.root.keys))    print("查找 40:", tree.search(40))    print("查找 55:", tree.search(55))    tree.show()
目的:用不到 100 行实现一个教学版 B+ 树,演示插入触发叶子/内部节点分裂、根提升以及树高的缓慢增长。
  1. class BPlusNode定义节点,is_leaf 区分叶子与内部节点,next_leaf 实现叶子链表。
  2. _find_leaf从根开始二分查找,沿着孩子指针一路下沉到叶子,模拟点查的树高次 IO。
  3. if len(node.keys) > self.max_keys叶子插入后超出最大键数,触发分裂。
  4. right.keys = node.keys[mid:]把右半键值分给新叶子,左半留给原叶子,中间键上提。
  5. right.next_leaf = node.next_leaf维护叶子链表的串联关系,保证范围查询可顺序扫描。
  6. node.keys.insert(i, up_key)内部节点收到子节点上提的键,插入到对应位置。
  7. new_root.keys = [up_key]根节点分裂时新建根,树高加 1。
  8. search从根到叶子走完整路径,体现 B+ 树查询稳定性。
  9. height沿最左孩子一路向下统计层数,观察高扇出带来的低树高。

常见误区

误区以为 B+ 树就是把二叉树每个节点多塞几个键,本质还是“两个孩子的树”。

正解B+ 树是真正的多路平衡查找树,阶数 M 可达数百上千,非叶节点有 M 个孩子但只存键;叶子节点存全部数据且串成链表。这些结构性差异让它具备了二叉树无法企及的高扇出、低树高和范围扫描能力,不是简单的“加宽版本”。

为何常见:教科书和博客为了画图方便,常把 B+ 树画成 3-4 阶的“小树”,让读者误以为它和二叉树只是规模区别,忽略了数据库里 M 以千计的关键现实。

误区查询慢就多建几个索引,反正 B+ 树查得快,字段都建上总没错。

正解每个索引都是一棵独立的 B+ 树。插入、删除、更新都要同步维护每一棵树的结构,可能触发节点分裂与页分裂,写放大明显;索引还额外占用磁盘与内存。读多写少的表可以多建索引,写多读少的表要克制。

为何常见:只看到了 SELECT 路径的收益,忽略了 INSERT/UPDATE/DELETE 路径上的维护成本,等到写入变慢或磁盘暴涨才回头排查。

误区B 树的键在中间层就能查到,B+ 树永远要走到叶子层,感觉 B+ 树更“吃亏”。

正解恰恰相反。非叶节点不存数据,同一页才能容纳更多键,扇出成倍提升、树高骤降;所有查询都走完整的根到叶路径,换来的是稳定的 IO 延迟。三层 B+ 树的最坏情况也只有 3 次 IO,远好于 B 树偶尔“少走一层”带来的微小收益。

为何常见:把“少比较几次”当成性能核心,忽略了磁盘 IO 的总次数才是真正的瓶颈。

误区以为 `BETWEEN` 查询对每个值都要走一次根到叶子的完整查找,代价非常高。

正解B+ 树只需一次点查找到第一个命中的叶子,然后靠叶子节点间的链表指针顺序向后扫描即可。范围查询的 IO 约等于“树高 + 命中叶子数”,而不是“每个值各走一遍树”。

为何常见:沿用二叉查找树的遍历心理模型,忽略了叶子链表这个专为范围扫描设计的结构。

面试题

Q1. B+ 树与普通二叉树在结构上的本质区别是什么?

单选题 · easy

  • A.B+树每个节点可有多个孩子且非叶只存键← 正确答案
  • B.B+树是一个完全平衡的满二叉树
  • C.B+树只有根节点和叶子节点两层
  • D.B+树的每个节点必须存放完整数据行

答案:A。B+ 树是多路平衡查找树,M 阶节点最多 M 个孩子,非叶节点只存索引键;二叉树只有两个孩子,且通常都要承担存储。所谓“两层”或“满二叉树”都不符合 B+ 树的定义。

Q2. B+ 树叶子节点之间用链表相连,主要目的是什么?

单选题 · easy

  • A.让范围查询可以顺序扫描叶子页← 正确答案
  • B.降低插入数据时的比较次数
  • C.减少叶子节点占用的存储空间
  • D.让根节点能直接访问所有叶子

答案:A。叶子链表把有序叶子串成一条链,范围查询找到起点后沿链表扫描即可,无需回根。它不改变插入比较次数,也不减少存储空间,更像是图书馆走廊之间的连通门。

Q3. 一棵 4 阶 B+ 树(M=4)的叶子节点最多能放几个键?

单选题 · easy

  • A.2 个
  • B.3 个← 正确答案
  • C.4 个
  • D.8 个

答案:B。M 阶节点的键数上限是 M-1,所以 4 阶节点最多 3 个键;一旦插入第 4 个键就会触发分裂。这对应图书馆书架的“容量上限”约束。

Q4. 为什么高扇出能显著减少磁盘 IO?

单选题 · mid

  • A.扇出越大树高越小,查找路径经过的页更少← 正确答案
  • B.扇出越大每个数据页的体积越大
  • C.扇出越大磁盘的读写速度就越快
  • D.扇出越大越不需要使用页缓存

答案:A。树高约等于 log_M N,扇出 M 越大树越矮,从根到叶的 IO 次数越少。页大小和磁盘速度与扇出没有因果关系,页缓存也不是由扇出决定的。

Q5. 关于 B+ 树非叶节点不存数据,下列说法正确的是?

单选题 · mid

  • A.非叶节点只存索引键,扇出更大树更矮← 正确答案
  • B.非叶节点不存数据导致查询必须全表扫描
  • C.非叶节点不存数据是为了节省内存
  • D.非叶节点不存数据会增加写放大

答案:A。非叶节点只存键和指针,同样 16KB 页能容纳的索引项大幅增加,扇出提升、树高下降。它不导致全表扫描,也不是为了省内存,写放大由索引数量决定。

Q6. InnoDB 中二级索引叶子节点存储的是什么?

单选题 · hard

  • A.完整的数据行记录
  • B.索引键加对应主键值← 正确答案
  • C.数据页在磁盘上的物理地址
  • D.指向根节点的起始指针

答案:B。二级索引的叶子存“索引键 + 主键”,查询时先找到主键,再回聚簇索引取整行。完整数据行在聚簇索引叶子页中,物理地址和根指针都不是 InnoDB 的存储方式。

Q7. 一棵 3 层聚簇索引 B+ 树,扇出约 1000,叶子页每页约存 16 行 1KB 数据,约能支撑多少行?

单选题 · hard

  • A.约 1 万行
  • B.约 16 万行
  • C.约 1600 万行← 正确答案
  • D.约 160 亿行

答案:C。根和中间层两层索引能指向 1000×1000=100 万个叶子页,每页 16 行,共约 1600 万行。这是 InnoDB 三层 B+ 树承载千万级表的经典估算。

Q8. 关于 B+ 树插入触发节点分裂,下列说法正确的是?

多选题 · mid

  • A.叶子节点键数超过上限时触发分裂← 正确答案
  • B.分裂后左右节点键数尽量保持均衡← 正确答案
  • C.分裂会把数据行直接复制到根节点
  • D.内部节点可能因孩子分裂而向上连锁分裂← 正确答案

答案:ABD。叶子满则分裂,中间键上提;父节点若因此溢出继续分裂,直到根。分裂左右尽量均衡。数据行只留在叶子,不会复制到根,所以 C 错误。

Q9. B+ 树非叶节点不存数据带来了哪些收益?

多选题 · hard

  • A.同一页能容纳更多索引键,扇出更大← 正确答案
  • B.树高降低,磁盘 IO 次数更少← 正确答案
  • C.所有键的查询路径长度一致,延迟稳定← 正确答案
  • D.非叶节点不再需要孩子指针

答案:ABC。非叶只存键让页内索引项变多、树矮、IO 少;所有查询都走完整路径,延迟稳定。但非叶节点仍需要孩子指针来导航,D 错误。

Q10. 关于二级索引回表,以下说法正确的是?

多选题 · mid

  • A.二级索引查到主键后需回聚簇索引取整行← 正确答案
  • B.覆盖索引可以让查询免于回表← 正确答案
  • C.回表通常需要额外的磁盘 IO← 正确答案
  • D.主键查询也需要回到二级索引回表

答案:ABC。二级索引只存主键,取完整行要回聚簇索引,通常增加 IO;若索引覆盖所需列则无需回表。主键查询直接在聚簇索引上完成,不需要回表,D 错误。