Files

154 lines
8.0 KiB
Markdown
Raw Permalink Normal View History

2026-08-09 19:06:40 +08:00
---
tags: [test/review, mysql, bplus-tree, clustered-index, secondary-index]
create time: 2026-08-09 12:00
---
# B+树索引原理_测试题
## 概述
本测试涵盖 InnoDB B+树索引的核心概念,包括 B 树与 B+树的本质差异、页分裂/合并机制、聚簇索引与非聚簇索引的区别,以及联合索引设计原则。共 10 道题:6 道选择题、3 道填空题、1 道简答题。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
InnoDB 存储引擎默认使用的索引数据结构是什么?
A. 哈希表
B. B+树
C. 红黑树
D. 跳表
### Q2(基础)→ 考察行为差异
以下关于 B 树和 B+树的说法中,正确的是哪一项?
A. B 树的非叶子节点也存储数据记录
B. B+树的查询路径长度不一致,取决于数据位置
C. B 树的叶子节点通过链表连接,支持高效范围查询
D. B+树在同样大小的页中能容纳更多键,因此树更高
### Q3(进阶)→ 考察原理理解
假设一个 InnoDB 表的页大小为 16KB,每条索引记录约 1KB,则单个页大约能容纳多少条索引键?在此条件下,1 亿行数据的 B+树高度通常是多少层?
A. 约 16 条,树高 5~6 层
B. 约 16384 条,树高 3~4 层
C. 约 1024 条,树高 4~5 层
D. 约 100 条,树高 6~7 层
### Q4(进阶)→ 考察比较辨析
为什么推荐使用自增 BIGINT 而非 UUID 作为主键?以下解释最准确的是:
A. UUID 太长,导致 InnoDB 无法为其建立索引
B. 所有二级索引叶子节点都存储了主键值,UUID 乱序插入会导致频繁页分裂且二级索引体积庞大
C. UUID 不唯一,可能产生主键冲突
D. 自增主键的查询速度是 UUID 的两倍以上
### Q5(深入)→ 考察场景推理
某业务表的主键为自增 BIGINT,当前填充率已达 85%。此时大量并发 INSERT 导致的页分裂策略是:
A. 每次都是严格的二分平分
B. 首次分裂占 7/8,后续改为平分,以预留碎片空间
C. 只有根节点满时才向上分裂,子节点不会立即分裂
D. 先删除旧数据再合并,避免产生碎片
### Q6(深入)→ 考察源码级细节
在二级索引的叶子节点中,实际存储的内容是:
A. 完整行数据
B. 索引列的值 + 回滚指针(rollback pointer)
C. 索引列的值 + 主键值
D. 索引列的值 + 该行的物理磁盘地址
---
## 二、填空题(3道)
### F1 — 聚簇索引的定义
InnoDB 中被称为"聚簇索引"的实际上是______索引,也就是说数据和索引存储在同一个______中。一张表有且仅有______个聚簇索引。
> **提示**: 聚簇的含义指的是数据文件本身就是按 B+Tree 组织的一份索引结构,想一想哪个索引直接包含了整行数据。
### F2 — 页分裂规则
当父节点也已满时,页分裂会______向上进行,直到找到不满的父节点或到达根节点。如果根节点发生分裂,则树的______会增加 1。
> **提示**: 分裂是从叶子节点向上传递的,根节点分裂是树结构变化的一个重要标志。
### F3 — 联合索引设计
在设计复合索引 `(col1, col2, col3)` 时,区分度高的列应放在______;等值查询列应在范围查询列之______;若 `WHERE col1 = 'a' AND col2 > 10 AND col3 = 'b'`,则只有前______列能有效利用索引过滤。
> **提示**: 回忆最左前缀原则和范围查询断链陷阱——遇到范围查询时,后面的列就无法使用前缀匹配了。
---
## 三、简答题(1道)
### S1
某电商订单表 `orders` 有以下字段:`id`(BIGINT 自增主键)、`user_id`、`status`、`created_at`、`amount`。现有以下两个高频查询:
```sql
-- 查询 1:按用户查订单列表
SELECT id, status, created_at FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
-- 查询 2:统计各状态的订单总数
SELECT status, COUNT(*) FROM orders GROUP BY status;
```
请综合分析:
1. 为这两个查询分别设计索引方案
2. 解释为什么要这样设计(考虑区分度、最左前缀、覆盖索引等因素)
3. 说明这种设计对 INSERT/UPDATE 操作可能带来的影响
> **答题框架提示**: 先分析每个查询的过滤条件和排序需求 → 判断哪些列适合做联合索引 → 评估是否覆盖所有 SELECT 列 → 考虑索引维护成本。
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | InnoDB 的默认索引结构是 B+树。哈希表不支持范围查询;红黑树是多路平衡树但在磁盘 IO 场景下不如多路树高效;跳表主要用于内存场景。 |
| Q2 | A | B 树的每个节点都存储数据和键,而 B+树的非叶子节点只存键不做数据负担,所以 A 正确。B 错误因为 B+树所有查询路径等长;C 错误因为 B 树叶节点没有链表连接;D 错误因为 B+树同样数据量下树更矮而非更高。 |
| Q3 | B | 16KB / 1KB ≈ 16 条是错误的估算方式——实际上索引键通常远小于 1KB,加上指向子节点的指针后每页可放下约 16000+ 个键。以每页约 1000~2000 个键计算,1 亿行数据的树高通常在 3~4 层。 |
| Q4 | B | 核心原因有二:① UUID 长度 36 字节,远大于 BIGINT 的 8 字节,导致所有二级索引体积膨胀,内存缓存命中率下降;② UUID 随机性导致插入顺序无序,引发频繁的页分裂和碎片。A 错在 InnoDB 可以为 UUID 建索引;C 错在 UUID 设计保证全局唯一;D 无依据。 |
| Q5 | B | InnoDB 采用"首次分裂占 7/8,后续平分"的策略来预留碎片空间,避免频繁分裂。这是面试常考的优化细节。A 错在不是严格二分;C、D 描述的策略不存在于 InnoDB 实现中。 |
| Q6 | C | 二级索引叶子节点存储的是(索引列的值 + 主键值),通过主键可以回表到聚簇索引获取完整行。A 是聚簇索引叶子节点的内容;B 中的回滚指针用于 MVCC,不在二级索引中体现;D 的物理磁盘地址 InnoDB 不暴露给存储引擎外部。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | 主键(或"聚簇");数据文件(或"InnoDB 表");1 | 聚簇索引的特点是数据文件和索引文件是同一份,叶子节点存储整行完整数据,每张表只能有一个聚簇索引。 |
| F2 | 递归;高度 | 父节点满了要递归向上分裂,这是 B+树的经典再平衡操作。根节点分裂意味着需要新增一层,树高度 +1。 |
| F3 | 前面;前;2 | 区分度高的列在前可以更早缩小查找范围;等值查询在前避免被范围查询中断最左前缀;`col1 = 'a'` 等值命中,`col2 > 10` 范围命中,但 `col3` 因 `col2` 的范围查询断链而无法使用索引。 |
### 简答题参考答案
**S1 参考答案要点**:
1. **查询 1 索引**:建议建立联合索引 `(user_id, created_at)`。`user_id` 是高区分度的等值过滤列,放前面可以快速定位;`created_at` 放后面可以直接满足 `ORDER BY` 排序需求,避免 filesort。同时 `id` 和 `status` 隐含在主键中,不需要额外覆盖。
2. **查询 2 索引**:建议建立 `(status)` 单列索引。`GROUP BY status` 可以利用索引的顺序分组,减少临时表的使用。虽然 `COUNT(*)` 需要从主键获取,但由于状态数通常很少(如待支付、已发货、已完成等),回表开销很小,不值得为这个查询单独建立覆盖索引。
3. **INSERT/UPDATE 影响**:增加索引会提高写入成本。每个 INSERT 需要在所有索引的 B+树中插入记录,可能导致页分裂。尤其是 `(user_id, created_at)` 联合索引,当 `created_at` 无序时也会触发分裂。可通过预排序批量插入或使用自增 ID + 顺序插入来缓解。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点(索引设计 × 2 + 原因分析 × 1 + 写入影响 × 1)。
## 关联笔记
- [[02.MySQL/index/B+树索引原理]]