想象你是城南图书馆的管理员小艾。馆里几万本书整整齐齐码在四排书架上,每排书架只能放几十本,再多就摆不下。读者来借书时,小艾从不一排排翻找:她先查门口检索台的总目录卡,再跟着楼层的引导牌缩小范围,最后直奔目标书架。如果读者说“我要 B 区到 M 区的所有书”,她就沿着书架之间的通道一路扫过去,不用再回总目录。这套“书架 + 引导牌 + 通道”的组合拳,正是数据库索引的底层结构——B+树。这篇文章将带你走进这棵树的内部:节点里究竟存什么、为什么查得这么快、主流数据库如何落地它、以及你建表建索引时该参考哪些经验。

岗位关联

读懂 B+树的节点结构与索引原理,你就能在搭建特征存储、设计训练数据管道时规避慢查询与回表陷阱,让 AI 系统的数据底座跑得更稳更快。

目录
  1. 可视化演示
  2. 节点家族三成员
  3. 内部节点与扇出
  4. 叶子链表与查询
  5. 盘页对齐设计
  6. 分裂合并平衡
  7. 数据库实现差异
  8. 索引策略与优化
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示B+树的内部节点扇出、叶子节点链表结构与范围查询过程,揭示数据库索引的高效搜索原理。

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

B+树索引结构 内部节点:仅存键值与子节点指针 叶子链表:顺序连接支持范围查询 高扇出:降低树深度减少磁盘IO Depth = log_f(N), IO = O(log_f N) B+树通过高扇出扁平结构实现高效索引

节点家族三成员

图书馆的“找书系统”分三级:入口的总目录卡、每层的引导牌、还有真正放书的书架。小艾有一个严格规定——引导牌上绝不堆书,书架上绝不兼职指路。各司其职,整个找书流程才不会乱。

B+树的节点也分三类:根节点、内部节点(统称非叶子节点)和叶子节点。根节点是树的唯一入口,内部节点是层层分流的“引导牌”,而所有真正的数据记录,只存放在叶子节点中。这与普通 B 树“每个节点都可以存数据”的朴素设计截然不同,是 B+树性能优势的起点。

一个内部节点里放着若干“键”和若干“指针”:键是分隔值,指针指向覆盖对应键范围的子节点。规律很清晰——n 个键把一维区间切成 n+1 段,所以指针数总是比键数多 1。查询从根出发,每层根据目标值与键的比较选一条指针下降,直到叶子。

Nptr=Nkey+1\displaystyle N_{ptr} = N_{key} + 1

1. N_key(键数):节点中用于分隔子区间而存放的键的个数,相当于引导牌上的书号边界。

Nkey\displaystyle N_{key}

2. N_ptr(指针数):指向下一层子节点的指针个数,相当于引导牌上的“箭头”数量。

Nptr\displaystyle N_{ptr}

3. +1 的来源:n 个键把范围切成 n+1 段,每段必须有一个指针,所以指针数恒为键数加一。

Nptr=Nkey+1\displaystyle N_{ptr} = N_{key} + 1

小艾很清楚:引导牌上写 3 个书号区间,就得画 4 个箭头;书架永远不会出现在引导牌那一层。这份“分工洁癖”正是 B+树维持高效的根本。

内部节点与扇出

引导牌尺寸固定,一行写一个“范围 → 箭头”。小艾发现,决定找书快慢的关键变量是:一张引导牌上能写多少行。写得多,读者被疏散到更多子区域,平均要多走的路就少——这个“一块牌能带几条路”的能力,在 B+树里叫扇出(fanout)。

在 B+树中,内部节点的容量由所在页(page)大小决定,每一行配置是一个“键 + 指针”对。于是扇出约等于页大小除以单个键与指针的字节数总和。键越短、指针越紧凑,扇出越大,树就越“矮胖”。

假设页大小 P,平均键长 K,指针长 T,则扇出:

m≈⌊PpageKkey+Tptr⌋\displaystyle m \approx \left\lfloor \frac{P_{page}}{K_{key} + T_{ptr}} \right\rfloor

InnoDB 默认 16KB 页,配上 8 字节的 bigint 主键和 6~8 字节的指针,扇出轻松上千。这意味着三层树就能覆盖百万级节点,四层就能支撑上亿行数据的索引。

1. P_page(页大小):节点可用的存储空间,也就是一次磁盘 I/O 能读入的数据量。

Ppage\displaystyle P_{page}

2. K_key + T_ptr(键指针组合):内部节点中一行“键 + 指针”占用的平均字节数。

Kkey+Tptr\displaystyle K_{key} + T_{ptr}

3. 相除取整:总字节数除以单行字节数得到可容纳行数,向下取整保证不越界。

⌊PpageKkey+Tptr⌋\displaystyle \left\lfloor \frac{P_{page}}{K_{key} + T_{ptr}} \right\rfloor

于是小艾规定:引导牌的标签一律用最简短的编号(“B~M“而不是”文学类 B 区到文学类 M 区”),一行塞下的信息越多,读者少跑两层楼。这不是经验玄学,而是有公式可算的工程权衡。

叶子链表与查询

读者想借“B 到 M 区所有书”。小艾的聪明办法是:书架之间修一条前后贯通的通道,找到起点书架后,顺着通道一本本往后取,全程不用回总目录。在 B+树里,这条通道就是叶子节点之间的双向链表。

叶子节点按键升序存放完整记录,且每个叶子都有 prev / next 指针,串成一条有序的“叶子链表”。这个设计的直接收益是范围查询:先沿树找到左端点所在的叶子(需要 h 次 I/O),再沿链表顺序扫描即可,完全不必逐层回溯。

设树高为 h,每个叶子页平均容纳 L 条记录,范围查询要取 r 条记录,则 I/O 次数约为:

Trange=h+⌈rL⌉\displaystyle T_{range} = h + \left\lceil \frac{r}{L} \right\rceil

1. h(树高定位):从根到左端点叶子的路径长度,是范围查询的固定开销。

h\displaystyle h

2. r / L(扫描叶页数):结果数量除以每页记录数,就是需要顺序读取的叶子页数。

rL\displaystyle \frac{r}{L}

3. 向上取整:即使某页只读一条记录也要一次 I/O,所以按页数向上取整。

⌈rL⌉\displaystyle \left\lceil \frac{r}{L} \right\rceil

小艾总说:“书架间的通道比天花板上的航拍图重要。” 点查靠树找对书架,批量取书靠通道顺藤摸瓜——这正是数据库 WHERE key BETWEEN ... 能秒回的原因。

盘页对齐设计

图书馆的搬运小车是定制的:一次刚好能装一格书。如果书架格子比小车大,一趟要跑两趟;比小车小,装不满浪费。所以小艾把每一格都按小车容量设计,不多不少。

磁盘读写以“页”为最小单位,数据库一次 I/O 至少读入一整页。B+树的每个节点大小刻意与磁盘页对齐:一个节点恰好占一页(InnoDB 默认 16KB、PostgreSQL 默认 8KB)。这样一次 I/O 就能读入一个完整节点,不浪费任何字节。

点查询沿树下行 h 层,每层访问一个节点就是一次页 I/O,因此:

Tpoint=h×Tio\displaystyle T_{point} = h \times T_{io}

其中 T_io 是单次磁盘 I/O 的耗时(毫秒级)。树高 3~4 层的 B+树,点查询固定只要 3~4 次 I/O,与数据总量几乎无关——这正是索引能“扛住亿级数据”的底气。

1. h(层数):点查询从根到叶子,每层只访问一个节点。

h\displaystyle h

2. T_io(单次 I/O 耗时):读一个节点对应一次磁盘页 I/O,是查询的主要成本。

Tio\displaystyle T_{io}

3. 相乘得总开销:总耗时约等于层数乘以单次 I/O 时间,所以减少树高就是缩短延迟。

Tpoint=h×Tio\displaystyle T_{point} = h \times T_{io}

小艾把书架、小车、通道都按同一个“格”来设计——对齐的代价是前期测量,收益是每次搬书都一步到位。数据库里页大小与节点大小的对齐,同理。

分裂合并平衡

某天“推理小说”书架满了,新书没处放。小艾的做法是:把书架从中间拆成两个,中间那本书“升级”到引导牌上当新路标,两边各放一半。过几天隔壁书架太空了,她又把两架合并。这样书架永远不溢出,也永远不浪费。

B+树插入时先定位叶子,若节点已满(键数达到上限 m),则分裂为两个节点,并把中间键提升到父节点继续充当分界。若父节点也因此变满,则继续向上分裂,直到根——根分裂时树整体增高一层。删除时若节点过空,则向兄弟借键或合并,必要时向下级联调整。

对满节点插入第 m+1 个键时,中间位置满足:

j=⌊m+12⌋\displaystyle j = \left\lfloor \frac{m+1}{2} \right\rfloor

左侧保留前 j 个键,第 j 个键作为“中间键”提升到父节点,剩余键交给右侧新节点。由于数据只存在叶子且提升键随时上移,B+树始终满足“所有叶子在同一层”,即绝对平衡。

1. m+1(临时候数):满节点插入新键后,节点里暂时有 m+1 个键。

m+1\displaystyle m+1

2. 取半下整(左侧键数):算出左侧节点应保留的键数 j,保证分裂后两边大致均衡。

⌊m+12⌋\displaystyle \left\lfloor \frac{m+1}{2} \right\rfloor

3. 中间键提升(新分界):第 j 个键上移到父节点,作为左右两个新区域的边界。

j=⌊m+12⌋\displaystyle j = \left\lfloor \frac{m+1}{2} \right\rfloor

书架的拆与并看似只是体力活,但小艾每次都要同步更新引导牌——对应数据库在分裂合并时对父节点的级联修改。平衡不是天生的,而是“拆了必须改路标”这套纪律换来的。

数据库实现差异

城北图书馆 A 馆规定:书必须按索书号排架,找到书架就是找到书,这叫“书跟号走”。城南借书处 B 馆则把书随意放,但每本都贴一张“位置纸条”:第几排第几格。两家都能快速找书,但内部分工完全不同。

MySQL InnoDB 与 PostgreSQL 正是这两派。InnoDB 使用聚簇索引:主键索引的叶子节点直接存放整行数据,表数据“跟着主键走”;二级索引的叶子则存主键值,查询要先走二级索引,再回聚簇索引拿整行。PostgreSQL 采用堆表:行数据放在堆(heap)中,索引叶子存放的是行的物理位置 TID(页号 + 偏移量),所有索引(含主键)都是非聚簇的。

在 InnoDB 中,二级索引等值查询通常要访问两棵 B+树:

Tsecondary=hsec+hclu\displaystyle T_{secondary} = h_{sec} + h_{clu}

h_sec 是二级索引树高,h_clu 是聚簇索引树高。此外两者页大小不同(16KB 与 8KB),导致扇出和树高存在差异。理解这些差异,才能解释“为什么同样的 SQL 在两个库里表现不同”。

1. h_sec(二级索引树高):先用二级索引这棵 B+树把目标主键找出来。

hsec\displaystyle h_{sec}

2. h_clu(聚簇索引树高):再拿主键回聚簇索引这棵 B+树读取完整行。

hclu\displaystyle h_{clu}

3. 相加(回表总开销):两棵树各走一遍,所以二级索引回表查询是最坏情况。

Tsecondary=hsec+hclu\displaystyle T_{secondary} = h_{sec} + h_{clu}

小艾去 A 馆参观后感叹:A 馆“号到书到”少一趟路,但改索书号等于重排满馆书架,成本高;自己用“位置纸条”灵活,却总要多看一眼纸。没有绝对好坏,只有围绕主键的取舍。

索引策略与优化

小艾总结出了一套选书架铁律:书号越短,引导牌能写的行越多(扇出大),树就越矮;书号递增,新书永远加在最后一格,书架难得拆。这四条经验,换成数据库术语就是主键选择与索引设计策略。

树高与扇出、记录数的关系是指数级的:

h=⌈log⁡mN⌉\displaystyle h = \left\lceil \log_{m} N \right\rceil

扇出 m 越大,树高增长越慢。假设扇出 1000,树高 3 即可覆盖百万级节点;若键变成又长又宽的 varchar(200),扇出骤降,树变高,每次查询多一层就多一次 I/O。

由此得出实用策略:其一,主键选小且有序(自增 bigint 最优),避免随机 UUID 引发大量页分裂与碎片;其二,按真实查询模式建索引,但不是越多越好——每多一个索引就多一棵 B+树,写入要同步维护,产生写放大;其三,用覆盖索引(联合索引包含所需全部列)让查询在索引页内拿齐数据,免回表。

1. N(记录数):要索引的数据总量,对应馆藏图书总册数。

N\displaystyle N

2. m(扇出):每个内部节点平均指向的子节点个数,对应引导牌的容纳行数。

m\displaystyle m

3. 以 m 为底的对数再上整:树高等于记录数按扇出取对数的上整,扇出翻倍树高减一。

h=⌈log⁡mN⌉\displaystyle h = \left\lceil \log_{m} N \right\rceil

小艾把四条铁律贴在工作台:号短、号涨、路牌字小稀疏、通道少而精。数据库里每一次建索引、选主键,其实都在回答同一个问题:你愿意为“读得快”付出多少“写得慢”的代价?

AI 应用场景

场景1:数据库慢查询诊断与索引重建

阶段:数据存储与特征管道

解决的问题:训练数据量增大后,特征表关联查询频繁超时。传统做法是“哪里慢就无脑加索引”,反而拖垮写入性能。理解 B+树节点与扇出后,能按查询模式(等值 or 范围)精准选择列与键类型,并用覆盖索引免回表。

具体案例:某推荐团队的特征表 user_feature(user_id BIGINT, day DATE, feat_xxx ...) 经常按 (user_id, day) 范围取 90 天特征。工程师将主键改为自增 bigint,并建立 (user_id, day) 联合覆盖索引,查询从全表扫描的 2.3 秒降到 23 毫秒。

场景2:特征存储与训练数据批量加载

阶段:训练数据准备

解决的问题:模型训练需要按时间范围批量拉取特征序列。基于对叶子链表范围扫描的理解,可以设计“时间有序追加”的主键与分区,让批量读取走顺序 I/O,而不是随机散读。

具体案例:在线广告 CTR 模型每天从特征库拉取用户 30 天行为序列。特征库采用 (date, user_id) 时间有序主键,读取时先定位起点叶子,再沿叶子链表顺序扫描,吞吐量提升数倍,训练数据准备耗时从小时级降到分钟级。

场景3:AI 应用在线服务的表结构设计

阶段:在线服务数据层

解决的问题:AI 应用上线后,用户行为日志表用 UUID 主键导致页分裂严重、写入变慢。基于 B+树分裂机制,改为趋势递增 id 后,新记录永远追加在右端叶子,写入退化为顺序 I/O。

具体案例:一个 LLM 对话服务把 session_log 表主键从 UUID 换成趋势递增 bigint 后,写入 P99 延迟从 45 毫秒降到 8 毫秒,索引碎片率下降 60%。

Python 演示

python104 行
import bisect class Node:    # B+树节点: 叶子存数据, 内部节点只存键和子指针    def __init__(self, leaf=False):        self.leaf = leaf        self.keys = []            # 键, 升序排列        self.childs = []          # 内部节点的子指针        self.vals = [] if leaf else None  # 叶子节点的数据        self.next_leaf = None     # 叶子链表后继  class BPlusTree:    # 简化版 B+树: 支持 insert / search / range_query    def __init__(self, order=4):        self.order = order        # 节点最大键数        self.root = Node(leaf=True)     def _find_leaf(self, key):        n = self.root        while not n.leaf:            i = bisect.bisect_right(n.keys, key)            n = n.childs[i]        return n     def _height(self, n):        if n.leaf:            return 1        return 1 + self._height(n.childs[0])     def search(self, key):        n = self._find_leaf(key)        i = bisect.bisect_left(n.keys, key)        if i < len(n.keys) and n.keys[i] == key:            return n.vals[i]        return None     def range_query(self, lo, hi):        n = self._find_leaf(lo)        res = []        while n is not None:            for k, v in zip(n.keys, n.vals):                if k > hi:                    return res                if k >= lo:                    res.append(v)            n = n.next_leaf        return res     def insert(self, key, val):        split = self._insert(self.root, key, val)        if split is not None:            left, mid_key, right = split            root = Node()            root.keys = [mid_key]            root.childs = [left, right]            self.root = root     def _insert(self, n, key, val):        if n.leaf:            i = bisect.bisect_left(n.keys, key)            n.keys.insert(i, key)            n.vals.insert(i, val)            if len(n.keys) <= self.order:                return None            mid = len(n.keys) // 2            right = Node(leaf=True)            right.keys = n.keys[mid:]            right.vals = n.vals[mid:]            right.next_leaf = n.next_leaf            n.next_leaf = right            n.keys = n.keys[:mid]            n.vals = n.vals[:mid]            return (n, right.keys[0], right)        i = bisect.bisect_right(n.keys, key)        split = self._insert(n.childs[i], key, val)        if split is None:            return None        left, mid_key, right = split        j = bisect.bisect_left(n.keys, mid_key)        n.keys.insert(j, mid_key)        n.childs.insert(j + 1, right)        if len(n.keys) <= self.order:            return None        mid = len(n.keys) // 2        up = n.keys[mid]        right_node = Node()        right_node.keys = n.keys[mid + 1:]        right_node.childs = n.childs[mid + 1:]        n.keys = n.keys[:mid]        n.childs = n.childs[:mid + 1]        return (n, up, right_node)  if __name__ == '__main__':    tree = BPlusTree(order=4)    lib = {5: 'Little Prince', 12: 'Algorithm',           25: 'Three Body', 32: 'Deep Learning',           40: 'Statistics', 58: 'LLM Ops'}    for k, v in lib.items():        tree.insert(k, v)    print('search 25 ->', tree.search(25))    print('range [12, 40] ->', tree.range_query(12, 40))    print('height ->', tree._height(tree.root))
目的:用约 90 行 Python 实现一棵支持插入、等值查询、范围查询的 B+树,直观验证“数据全在叶子、叶子链表串范围”两大核心机制。
  1. n = self.root; while not n.leaf从根出发,沿内部节点的“引导牌”逐层下降,直到进入叶子。
  2. i = bisect.bisect_right(n.keys, key)在内部节点键数组中二分定位,找到第一个大于 key 的位置作为下降子树下标。
  3. n.keys.insert(i, key); n.vals.insert(i, val)向叶子有序插入键值对,保持叶子内升序。
  4. return (n, right.keys[0], right)叶子分裂后返回“右节点最小键”,由上层把它提升为父节点的新分界。
  5. n.childs.insert(j + 1, right)父节点在正确位置挂上新子节点,保证键与指针的排列依然有序。
  6. while n is not None: ... n = n.next_leaf范围查询定位起点后沿叶子链表顺序扫描,收集落在区间内的所有记录。

常见误区

误区B+树不就是一棵树吗?内部节点存点数据还能省空间,反正都是存索引。

正解B+树的核心设计恰恰是“数据只存叶子”。内部节点只存键和指针,空间全部留给“路标”。这样同一个页能塞更多键 → 扇出更大 → 树更矮;同时数据只在一个地方,范围查询沿叶子链表走即可,不必在不同层级间横跳。若把数据塞进内部节点,扇出骤降、树变高、I/O 变多,得不偿失。

为何常见:很多人先接触 B 树或二叉搜索树,习惯了“每个节点都存数据”的模型,没意识到 B+树是“指路与藏书分离”的专门设计。

误区主键用 int 还是 varchar(128) 不都能区分记录吗?存得下就行,性能差别不大。

正解键值直接决定扇出:16KB 页、8 字节键时扇出上千;换成 200 字节的字符串键,扇出只剩下几十。扇出小 → 树高增加一层 → 每次查询多一次磁盘 I/O。这正是数据库规范总强调“主键要短小精悍”的原因。

为何常见:新手看不到“树高”这个隐藏变量,只关注存储容量,忽略了单条键对内部节点空间的挤占。

误区查询慢就加索引,反正磁盘便宜,把常用列全建上索引准没错。

正解每个索引都是一棵独立的 B+树。表数据增删改时,所有索引都要同步维护:页分裂、页合并、随机写……索引越多,写放大约严重。索引是“用写入性能换读取性能”的杠杆,应按真实查询模式挑选,能合并成联合索引的就合并。

为何常见:在小数据量、低写入压力的演示环境里,多建索引的代价看不出来,一上生产流量就原形毕露。

误区`WHERE time BETWEEN ...` 肯定很慢,因为它要把结果一条条回主表拿数据。

正解在 InnoDB 聚簇索引上,范围查询定位起点后沿叶子链表顺序扫描,相邻记录就在相邻页甚至同一页里,是高效顺序 I/O。真正慢的是二级索引上的大范围查询:每条命中都要回聚簇索引再取一次行,退化为随机 I/O。所以“慢”精确定义是“二级索引 + 大范围 + 回表”,而非范围查询本身。

为何常见:把“查询慢”简单归因于 SQL 用了范围,忽略了索引结构(聚簇 vs 非聚簇、覆盖与否)才是决定性因素。

面试题

Q1. B+树中真正存储完整数据(行记录或指向行的指针)的节点是?

单选题 · easy

  • A.根节点
  • B.内部节点
  • C.叶子节点← 正确答案
  • D.所有节点

答案:C。像图书馆只有书架藏书一样,B+树所有数据只放在叶子节点。根节点是“总目录”,内部节点是“引导牌”,它们只存键和指针,不存数据。

Q2. 一个内部节点存了 n 个键,它通常有多少个子指针?

单选题 · easy

  • A.n 个
  • B.n-1 个
  • C.n+1 个← 正确答案
  • D.2n 个

答案:C。n 个键把一维区间切成 n+1 段,每段需要一个子指针。相当于引导牌上 n 个书号区间对应 n+1 条走廊,所以指针数恒为键数加一。

Q3. B+树范围查询能高效顺序扫描的根本原因是?

单选题 · easy

  • A.根节点缓存
  • B.叶子节点间的双向链表← 正确答案
  • C.内部节点二分查找
  • D.页对齐设计

答案:B。范围查询定位左端点后,沿叶子双向链表顺序扫描即可,不用反复回上层。双向链表就是“书架间通道”,这才是 B+树范围查询快的根源。

Q4. 在 InnoDB 中,主键索引(聚簇索引)的叶子节点存放的是?

单选题 · mid

  • A.指向堆表的 TID
  • B.主键值
  • C.整行数据← 正确答案
  • D.哈希值

答案:C。InnoDB 聚簇索引的叶子直接存整行数据,相当于“书跟着索书号摆放”。A 是 PostgreSQL 堆表的做法,B 是 InnoDB 二级索引叶子的做法,D 与 B+树无关。

Q5. 其他条件不变,主键从 int 换成 varchar(128) 后,索引树最可能发生什么?

单选题 · mid

  • A.扇出变大
  • B.扇出变小,树变高← 正确答案
  • C.树高不变
  • D.叶子链表消失

答案:B。键变大 → 每行“键 + 指针”更宽 → 一页能放的行数(扇出)变小 → 同样的数据树变得更高 → 每次查询多几次磁盘 I/O。这正是主键要短小的原因。

Q6. 下列哪些做法会降低 B+树内部节点的扇出?

多选题 · mid

  • A.把页大小从 16KB 调成 8KB← 正确答案
  • B.用长字符串做主键← 正确答案
  • C.增加叶子链表
  • D.增加记录条数

答案:AB。扇出约等于“页大小 /(键 + 指针)”。页变小或键变宽,都会让每页能容纳的键指针组合变少。叶子链表和记录条数不参与内部节点容量计算,不影响扇出。

Q7. 假设扇出 m = 1000,树高 3 层(根 + 内部 + 叶子)的 B+树最多能有多少个叶子节点?

单选题 · hard

  • A.10^3
  • B.10^6← 正确答案
  • C.10^9
  • D.无法确定

答案:B。根节点有 1000 个子指针指向内部节点,每个内部节点又有 1000 个子指针指向叶子,所以叶子数最多为 1000 × 1000 = 10^6。每片叶子还能容纳多条记录,实际记录数远大于叶子页数。

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

多选题 · hard

  • A.分裂后两个节点键数大致各半← 正确答案
  • B.中间键会提升到父节点← 正确答案
  • C.分裂只发生在叶子节点
  • D.分裂可能逐层向上传播到根,导致树增高← 正确答案

答案:ABD。叶子满时插入会分裂为左右两个节点,中间键上提父节点;若父节点也因此变满则继续向上分裂,直到根,根分裂时树整体加高一层。内部节点同样会分裂,故 C 错误。

Q9. 覆盖索引(联合索引包含查询所需全部列)能带来哪些好处?

多选题 · mid

  • A.免去回表取行← 正确答案
  • B.减少磁盘 I/O← 正确答案
  • C.让树高必为 2
  • D.消除所有写放大

答案:AB。覆盖索引让查询在索引 B+树内就能拿齐所需列,不必回聚簇索引取整行,从而减少随机 I/O。但它不能改变树高,也不能消除新增索引带来的写放大,故 C、D 错误。

Q10. 对 1 亿行的 InnoDB 表做二级索引等值查询,假设二级索引与聚簇索引树高均为 4,最坏需要多少次页 I/O?

单选题 · extreme

  • A.4 次
  • B.8 次← 正确答案
  • C.16 次
  • D.2 次

答案:B。二级索引等值查询先沿二级索引树找主键,消耗 h_sec = 4 次 I/O,再回聚簇索引取整行,消耗 h_clu = 4 次 I/O,共 8 次。若直接拿主键查询只需 4 次——这正是“回表”的代价。