%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
%%
# InnoDB
InnoDB 是 MySQL 的 **==默认==引擎**,支持 **==事务处理==**(ACID)、**==外键==约束** 和 **==行级==锁定**,但**不支持全文本搜索**。
- **优点**:InnoDB 通过使用**行级锁**实现了高并发,支持**崩溃恢复**机制,能在崩溃后自动恢复数据。
- **适用场景**:需要事务处理、外键约束、并发控制的场景,比如金融系统、电子商务平台。
# InnoDB 的数据存储层次
InnoDB 底层采用以下 **逻辑层次结构** 来组织管理数据存储:
**表空间(Tablespace)→ 段(Segment)→ 区(Extent)→ 页(Page)**
| 层级 | 说明 |
| --------------- | ------------------------------------------------------------------------------------- |
| 表空间(Tablespace) | InnoDB 中最上层的存储单位,包含有多个 "**段**"。每个表空间存储着 **==1 个或多个表==** 的所有数据。 |
| 段(Segment) | InnoDB 存储**表数据**、**索引**、**回滚信息**等的**集合单元**,**段类型**根据**所存数据类型**划分,每个段包含**多个区**。 |
| 区(Extent) | InnoDB 进行**磁盘存储==分配==**的单位,每个区默认为 1MB(**64 个数据页**)。 |
| 页(Page) | InnoDB **存储数据的基本单位**,**每个页**存储实际的 "**表数据**" 或 "**索引数据**" 等数据,每个页默认为 **==16KB==**(可配置) |
> [!example] 存储层级示意
>
> ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 索引.assets/IMG-MySQL 索引-791453EEE052C63BB254A7BEC211329F.png]]
>
> 假设创建了一个 `user`表格:
>
> ```CREATE TABLE user(
> id INT PRIMARY KEY,
> name VARCHAR(100),
> age INT,
> INDEX (name)
> ) ENGINE=InnoDB;
> ```
>
> - 表空间:若 `innodb_file_per_table=ON`,数据存储在 `users.ibd` 文件中,否则**默认存储在 `idbdata` 文件中**。(共享表空间)
> - 数据段:存储实际**行记录** `(id, name, age)`;
> - 索引段:存储 `name` 列的**二级索引**。
> - 区:初始分配 1 个页,而后按需增长,每次以区为单位扩展。
>
>
<br><br>
## 表空间(Tablespace)
InnoDB 默认所有表格的数据均存储在 **`ibdata1` 文件**中(共享表空间),也可以设置 `innodb_file_per_table=ON` 来**为每张表创建独立的 `.ibd` 文件**。
表空间类型:
| 类型 | 说明 | 文件 |
| ----------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------- |
| 系统表空间(System Tablespace) | 默认情况下,存储**所有表的数据**(即 `innodb_file_per_table=OFF` 时) | 默认是 `ibdata1`,可以配置为多个 `ibdataX` 文件。 |
| 独立表空间(File-Per-Table Tablespace) | 启用 `innodb_file_per_table=ON` 后,每张表自动对应一个独立文件,存储**表的数据和索引** | 每张表对应一个**独立的 `.ibd` 文件**,文件名格式为 `table_name.ibd`。 |
| 通用表空间(General Tablespace) | 自定义表空间,自定义文件名。 | 创建表空间时,自定义指定 `.ibd` 文件,例如 `tablespace1.ibd` |
| 临时表空间(Temporary Tablespace) | - 存储 **内部临时表**(如 `GROUP BY`, `ORDER BY` 需要的临时数据) <br>- 存储 **用户创建的 InnoDB 临时表**。 | 默认是 `ibtmp1` |
| REDO 日志空间(Redo Log Tablespace) | 存储 **事务的 Redo 日志** | 默认是 `ib_logfile0`, `ib_logfile1` |
| Undo 表空间(Undo Tablespace) | 存储 **事务的 Undo 日志** <br> | 默认是 `undo001`, `undo002` |
| 数据字典表空间(Data Dictionary Tablespace) | 存储 **InnoDB 的数据字典**(表信息、索引信息等元数据) | `mysql.ibd` |
> [!NOTE] 关于系统表空间
>
> - MySQL 8.0 以前,InnoDB 的数据字典、Undo Log 均存储于**系统表空间**中,对应文件 `idbdata1`。
> - MySQL 8.0 默认使用几个**独立表空间**,来独立存储上述数据,**避免 `idbdata1` 过度膨胀**。
> - Undo Tablespace:**存储 Undo 日志**( MySQL 8.0 默认使用两个独立的 Undo 表空间,可配置 `innodb_undo_tablespaces` 参数修改 )
> - Data Dictionary Tablespace:**存储 InnoDB 的数据字典**
>
> [!example] 创建自定义表空间 & 指定存储表的表空间
>
> ```sql
> -- 创建通用表空间, 指定表空间对应的文件名
> CREATE TABLESPACE myspace ADD DATAFILE 'myspace.ibd' ENGINE=InnoDB;
> -- 创建表格时, 指定其数据存储所属的表空间
> CREATE TABLE my_table(
> id INT PRIMARY KEY,
> name VARCHAR(255)
> ) TABLESPACE myspace ENGINE = InnoDB;
> ```
>
>
<br>
## 段(Segment)
| 类型 | 说明 |
| -------- | --------------------------------------- |
| **数据段** | 存储表的行数据。 |
| **索引段** | 存储**二级索引**的数据。**每个二级索引对应一个索引段**。 |
| **回滚段** | 存储**未提交事务**的回滚信息,用于 MVCC 和事务回滚 |
| **临时段** | 存储临时表或临时索引数据 |
| 系统段 | |
| 自适应哈希索引段 | 当 InnoDB 启用了 **自适应哈希索引** 时,会使用此段提高查询性能。 |
| 插入缓冲段 | 用于优化 **二级索引的插入**,提高批量插入的性能 |
> [!example] 每张表至少有 **1 个数据段**,每个**二级索引**都会增加一个==**索引段**==
>
> 下例中,存在 `idx_name` 与 `idx_age` 两个二级索引,因此有两个回滚段。
>
> ```sql
> CREATE TABLE users (
> id INT PRIMARY KEY,
> name VARCHAR(50),
> age INT,
> INDEX idx_name (name),
> INDEX idx_age (age)
> ) ENGINE=InnoDB;
> ```
>
>
<br>
## 区(Extent)
区是 InnoDB 进行**磁盘存储分配**的基本单位,每个区的大小默认为 **1MB**(64 个 16KB 数据页)
InnoDB 的磁盘空间分配方式为:
- **新表创建时,只分配 1 页(16KB)**;
- 当**表中数据增长**时,以 "**区**" 为单位进行扩展。(小表可能只按 4 页 (64 KB)扩展)
<br>
## 页(Page)
InnoDB **存储数据的最小单位**,每页默认大小为 16KB。
常见的页类型:
| 类型 | 说明 |
| ---------------- | ----------------------------- |
| 数据页(INDEX page) | 存储**表的行数据** (即 B-Tree 叶子节点) |
| 索引页(INDEX page) | 存储**索引**(即 B-Tree 内部节点) |
| 回滚页(UNDO page) | 存储**事务的回滚日志** |
| 系统页(SYSTEM page) | 存储 InnoDB 的内部管理信息 |
| 事务日志页(LOG page) | 存储事务日志数据 |
| 自由列表页(FREE page) | 存储未分配的数据页 |
<br><br><br>
# InnoDB 数据页
InnoDB 中数据的存储基于 「**==数据页==**」 进行组织管理,
同时,也是以 「**==数据页==**」为最小单位进行 I/O 操作——至少将 "**==一整页==**"从磁盘读入内存,或从内存刷入磁盘。
## 数据页结构
![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 索引.assets/IMG-MySQL 索引-415367CA527C2391A060950A3F84E8EF.png|761]]
重要部分说明:
- **文件头**:其中包含**两个指针**,指向 **聚簇索引的 B+树** 里**该页同层中的 prev、next 两个相邻页**。
- **用户记录**:即实际存储的**表数据**,每一条记录对应**表中的一行数据**。
- 页内行记录按 "**==主键顺序==存储**",记录间以 "**==单向链表==**" 形式组织。
- **页目录**:用于实现 "**页内索引**"。
- 存储着 "**每组最后一条记录**" 的页内 **==地址偏移量==**(按先后顺序),每一项称之为 "**槽**"(slot)——相当于**指向各组最后一项记录的指针**。
## 行记录的存储结构
在 InnoDB 数据页中,**每行数据** 的存储结构如下,每条 "**表格行记录**" 前面存储有额外的元数据:
![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL InnoDB 引擎.assets/IMG-MySQL InnoDB 引擎-B09A5E482E2DFD4A5E2A94BBC6BA1D16.png|825]]
- **行首元数据**
- **变长字段长度列表**:标记各个变长列的偏移量
- **NULL 值列表**:标记哪些列是 NULL
- 若表中存在允许为 NULL 的字段,则每行中至少用 **1 字节** 空间存储 NULL 值列表。
- **==记录头==**(Record Header)
- `NEXT_RECORD`:指向**页内下一记录**的偏移量。**数据页的各个行记录**之间,通过该指针构成 "**==单向链表==**"。沿着链表可遍历页内所有行。
- `HEAD_NO`:记录的唯一编号
- `RECORD_TYPE`:记录类型(普通行、删除标记行等)
- **行记录**
- **主键**
- **其它列数据**
- **隐藏字段**:
- `DB_ROW_ID`:隐藏自增 ID,在无主键时使用
- `DB_TRX_ID`:最近一次修改事务 ID
- `DB_ROLL_PTR`:指向回滚日志的指针
## 页目录
页内的记录会进行 "**分组**",如下图所示,除首尾两分组外,InnoDB 规定**每分组中记录数在 4~8 条**之间。
页目录中存储着 "**每组最后一条记录**" 的页内 **==地址偏移量==**(按先后顺序),每一项称之为 "**槽**"(slot)——即相当于**指向各组最后一项记录的指针**。
> [!NOTE] **页内查找某行记录的过程**
>
> 由于页内行记录**有序**排列,故可**在 slot 上进行二分查找**,**快速定位某个页==所在分组==**,再在**分组内沿着链表** 查找到目标页。
>
>
> ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL InnoDB 引擎.assets/IMG-MySQL InnoDB 引擎-55D113E5A889B20D22C2E67BFB038DDD.png|596]]
>
>
<br><br>
# Buffer Pool 缓冲池
InnoDB 引擎的 Buffer Pool 是一块内存区域,用于**缓存==数据页==、==索引页==、辅助信息**(插入缓冲、锁信息、数据字典等)。
其作用旨在 **减少对磁盘的直接访问次数**,提高数据库读写性能:
- 查询数据时,若命中缓存则直接读 Buffer Pool 中的内容;
- 修改数据时,若数据位于 Buffer Pool 中则直接修改 Buffer Pool 中的数据页,并标记为 "**==脏页==**"(内存与磁盘数据不一致),而后,由后台线程 **==异步==** 地将脏页写入到磁盘。
> [!NOTE] Buffer Pool 说明
>
> InnoDB 以 "**页**" 作为存储数据的基本单位,也即**内存 ↔ 磁盘间 I/O** 的基本单位,因此 **Buffer Pool 同样按 "页" 划分**,称之为 "**==缓存页==**",即对磁盘上 "页" 数据的内存缓存。
>
> ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 日志.assets/IMG-MySQL 日志-984FC56A75998BBEB52BF3D6E7CA7D73.png|813]]
>
<br><br><br>
# InnoDB 中的 B+树索引
> [!summary] 总结:"聚簇索引" 与 "二级索引"
>
> InnoDB 引擎下,**每张表对应一个==唯一的聚簇索引==**,**表中所有数据**都直接存储在 "**聚簇索引**" 对应的 **B+树的叶子结点**中。
>
> 除" **聚簇索引**" 外,InnoDB 中采用 **B+ 树** 结构的 **==普通索引==**、**==唯一索引==**、**==前缀索引==** 都属于 **二级索引**,**每个二级索引都是==一棵独立的 B+树==**。
>
> - 「**聚簇索引**」:B+树的叶节点存放 "**聚簇索引 Key**" & "**==实际数据==**",表格中所有记录都放在**聚簇索引的叶子节点**。
> - 「**二级索引**」:B+树的叶节点存放 "**==二级索引 Key==**" & "**==聚簇索引 Key==**",而不是实际数据。
>
> **数据**在物理上只会保存一份,所以**聚簇索引只有唯一一个**,而**二级索引可以创建多个**[^1]。
>
> ^5kf4d8
<br>
## InnoDB 聚簇索引(Clustered Index)
> 也称 "**主键索引**"。
InnoDB 中**每张表**对应一个唯一的 "**==聚簇索引==**",以 **B+树** 的方式组织, **表中数据** 直接存储在 "**聚簇索引构成的 ==B+树的叶子节点==**" 中,数据按 "**聚簇索引的 key**" 的顺序排列。
**创建表格时**,InnoDB 根据不同情况来确定**作为该索引的列**:
1. 若定义了 **==主键==**,则默认使用主键作为**聚簇索引的索引键**( Key);
2. 若未指定主键,则自动选择 **首个 `UNIQUE` 且 `NOT NULL` 的列** 作为 Key;
3. 若上述情况均无,则选取 "**隐藏字段**" 中的 `DB_ROW_ID`(隐藏自增 ID)作为 key。
> [!example] 聚簇索引的 key
>
> ```sql
> CREATE TABLE users (
> id INT PRIMARY KEY, -- 主键 `id` 作为聚簇索引的 key
> name VARCAHR(50),
> age INT
> ) ENGINE=InnoDB;
> ```
>
> ```sql
> CREATE TABLE users (
> name VARCHAR(20) UNIQUE,
> uid INT UNIQUE NOT NULL, -- `uid` 作为聚簇索引
> age INT
> ) ENGINE=InnoDB;
> ```
>
> ^8luk98
> [!important] InnoDB 中 "聚簇索引" 对应的 B+树
>
> - 树节点内容是 "**数据页**",每个数据页**默认大小为 16KB**。
> - **表中所有数据**直接存储于 "**聚簇索引**" 对应的 **B+树的叶子节点** 中。
>
> ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 索引.assets/IMG-MySQL 索引-71AF7E2627F9BC7D647E67F813E5364A.png|973]]
>
>
> 每个数据页的 **"文件头" 字段** 中有两个指针,连接 B+树里**同层的相邻两个数据页**,每一层均构成 **==双向链表==**,如下图所示:
>
> ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 索引.assets/IMG-MySQL 索引-00679C89578EEA019133F16520821B00.png|684]]
>
>
<br>
## InnoDB 二级索引(Secondary Index)
> 也称**辅助索引**、**普通索引**。
「**二级索引**」:B+树的叶节点存放 "**==二级索引 Key==**" & "**==聚簇索引 Key==**",而不是实际数据。
除" **聚簇索引**" 外,InnoDB 中采用 **B+ 树** 结构的 **==普通索引==**、**==唯一索引==**、**==前缀索引==** 都属于 **二级索引**,**每个二级索引都是==一棵独立的 B+树==**。
一张表可以有**多个二级索引**,**每个二级索引都是一棵独立的 B+树**。
> [!NOTE] 二级索引查询过程:**==回表==**(Bookmark Lookup)
>
> 在**二级索引**的 B+树中,查找到**目标二级索引 Key** 对应的**聚簇索引 Key**,再据此去 "**聚簇索引**" 中查找 **==完整行数据==**。
>
> 该过程即称之为 **"回表"**: **二级索引-> 聚簇索引 -> 行记录**。
>
> ^yc1e9r
<br>
## 覆盖索引(Covering Index)
**覆盖索引**——**当 "二级索引本身" 包含了==查询所需的所有列==时,查询可以直接从索引中获取数据,而无需回表查询**。
二级索引对应的 **B+树叶节点**中,存储有 "**==索引列==**" 的值(单个或多个,即**单列索引 or 联合索引**)。
如果 "**查询的字段**" 完全包含在 "**二级索引列**" 中,则 **==无需 "回表"==**,在 **二级索引的 B+树叶节点** 即可返回所需数据,提升查询性能。
> [!example] 示例
>
> ```sql
> CREATE TABLE users (
> id INT PRIMARY KEY,
> name VARCHAR(50),
> age INT,
> ) ENGINE=InnoDB;
>
>
> -- 创建多列索引
> CREATE INDEX idx_name_age ON users (name, age);
>
> -- 查询: 字段均包含在索引中, 索引覆盖。
> SELECT name, age FROM users WHERE name = 'Alice';
>
> -- EXPLAIN 查看是否覆盖索引: 若显示 `Using index` 或 `Using index; Using where`, 表明覆盖索引
> EXPLAIN SELECT name FROM users WHERE name = 'Alice';
> ```
>
<br>
## 联合索引的 "最左匹配原则"
联合索引也称 "**复合索引**",泛指**建立在==多个列==上的索引**,索引相当于按 "**==多关键字==排序**"。
联合索引遵循 "**==最左匹配原则==**",具体如下:
1. 从**联合索引的==最左列==** 开始,**逐列匹配**。若**某一列==未出现在查询条件中==**,则索引对 **==后续列==** 不生效。
2. 若**对某一列使用了范围查询**,则:
- 对于 `<`, `>`,则联合索引**对 ==后续列== 不生效**。
- 对于 `<=`,`>=`,`BETWEEN`, `LIKE 'xx%'`,则联合索引**对 后续列 "==部分生效=="**。
> [!tip] 建立联合索引时,应当优先将 **"==区分度==" 较大** 的字段排在前
>
> $
> \text { 区分度 }=\frac{\operatorname{distinct}(\operatorname{column})}{\operatorname{count}(*)}
> $
>
> [!example] 联合索引示例
>
> 例如,对三个列建立联合索引 `(a, b, c)`,
>
> 联合索引生效的情况(注:有**查询优化器**,故**索引字段在 where 子句的==出现顺序无关==**,存在即可)
>
> - `where a = 1;` ——联合索引对 a 生效
> - `where a = 1 and b = 2 and c = 3;`——联合索引对 a、b、c 生效
> - `where a = 1 and b = 2;` ——联合索引对 a、b 生效
> - `where a > 1 and b = 2;` ——联合索引**仅对 a 字段生效**,由于 a 使用了范围查询,故对 a 之后字段不生效。
> - `wher a = 1 and b = 2 and c > 3;` ——联合索引对 a、b、c 生效
>
> 联合索引不生效的情况(下列原因均是**字段 a 未出现**,**故联合索引对 a 之后的字段均不生效**)
>
> - `where b = 2;`
> - `where c = 3;`
> - `where b = 2 and c = 3`;
>
> [!note] 🚨 前一索引使用"范围查询" 时,对后续索引列 "部分生效" 的含义
>
> 指前一列存在 "**==等值条件==**" 的情况下,可以进而 **用上==后一列==** 定位起点,然后再往后进行范围查找。
>
> 示例一:存在联合索引 `(a,b)`
>
> ```sql
> -- 联合索引对字段b不生效.
> -- 原因在于 a > 1的部分, b值是无序的, 索引无法用上 b = 2 进行定位.
> SELECT * FROM my_table WHERE a > 1 AND b = 2;
>
> -- 联合索引对字段b部分生效. 索引在a=1的情况下, 可以再利用b=2进行定位, 然后再往后顺序遍历, 故用上了字段b
> SELECT * FROM my_table WHERE a >= 1 AND b = 2;
>
> -- MySQL中BETWEEN包含边界值, 故联合索引对字段b部分生效(仅限与 a = 2 && b = 2 与 a=8 && b=2的部分)
> SELECT * FROM my_table WHERE a BETWEEN 2 AND 8 AND b = 2;
> ```
>
>
> 示例二:存在联合索引 `(name, age)`
>
> ```sql
> -- 联合索引对name, age列均有效, 会直接定位到 name = 'j%' 且 age = 22的记录.
> SELECT * FROM my_table WHERE name like 'j%' and age = 22;
> ```
>
> 示例二说明[^2]:
>
> ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL InnoDB 引擎.assets/IMG-MySQL InnoDB 引擎-DAFC6F8D11D0CF40F8E72713F3C96A23.png|880]]
>
<br>
## "索引条件下推" 优化
> 索引下推优化(Index Condition Pushdown,**ICP**),MySQL 5.6 引入。
「**索引下推**」:针对 "**==联合索引==**",将**部分 WHERE 条件过滤**下推到 "**==存储引擎层==**" 执行(原由 Server 层执行),从而 **==减少回表次数==**。
具体说,即 **联合索引** 中 **部分列==未被匹配==**,但其**在 WHERE 子句里==用作了过滤条件==** 时,过滤将在引擎层完成。
> [!example] 示例
>
> ```sql
> CREATE TABLE users (
> id INT PRIMARY KEY,
> name VARCHAR(100),
> age INT,
> city VARCHAR(100),
> INDEX idx_name_age_city (name, age, city) -- 联合索引
> ) ENGINE=InnoDB;
>
> -- 查询: 联合索引仅对`name`列生效
> SELECT * FROM users WHERE name = 'Alice' AND city = 'New York';
> ```
>
> 对于上述查询语句,联合索引**只对 `name` 列生效**。
>
> - MySQL 5.6 之前,是**对所有 `name = Alice` 的数据进行 "==回表=="**,取得完整行数据后,**Server 层再根据 `city = 'New York'` 过滤**。
> - MySQL 5.6 引入 ICP 后,由于 **`city` 列本身也在联合索引中**,故会在引擎层,**直接在==索引检索过程中==根据 `city = 'New York'` 过滤**,最后**只需对同时满足 `name = Alice AND city = 'New York'` 的数据进行回表**。
>
<br><br><br>
# InnoDB 中的倒排索引
> 倒排索引 Inverted Index,即 **`Full-Text` 索引方式** 的底层实现。
InnoDB 自 MySQL 5.6 起支持 `FULLTEXT`,其实现方式与 MyISAM 略有不同(MyISAM 使用**独立的倒排索引文件**)。
InnoDB 的 **倒排索引** 基于多个 **==FTS 内部表==** 实现,**表中数据**本身仍基于 **==B+树==聚簇索引**的方式存储,**B+树的叶节点**存储表中数据。
几个 FTS 表共同维护了**倒排索引的数据信息**,包括:**词典**、**倒排列表**、**位置索引**、**停用词表**。
| Full-Text Search 表 | 存储内容 | 数据结构 |
| ------------------------------------- | -------------------------------------------------------------- | ---- |
| FTS_0000000000000001_0000000000000002 | **词典表**(Lexicon),存储 "**Token -> Token ID**" 的映射 | B+树 |
| FTS_0000000000000001_0000000000000003 | **倒排列表**(Posting List),存储 **Token ID -> (Doc_ID,词频) ** 映射 | B+树 |
| FTS_0000000000000001_0000000000000004 | **位置索引**(Position),存储 "**Token ID** -> **(Doc_ID, Pos)**" 映射 | B+树 |
| FTS_0000000000000001_0000000000000005 | **停用词表**(Stopwords),存储不参与索引的常见词(如 `the`, `is`, `and`) | 哈希表 |
## Full-texx 查询
> [!note] 一次 Full-text 查询涉及多个 FTS 表查询 + 回表操作
>
> ```sql
> -- 搜索包含 "MySQL"、"Index" 的行
> SELECT * FROM articles
> WHERE MATCH(content) AGAINST('MySQL Index');
> ```
>
> 整个查询过程大致为:
>
> 1. 用 Token 查**词典表**,获取 Token ID;
> 2. 用 Token ID 查**倒排列表**,获取 Doc ID;
> 3. 用 Token ID 查**位置信息**,获取 Token 在 Doc_ID 中出现位置;
> 4. 计算 BM25 评分:结合**词频 TF**、**逆文档频率 IDF**,确定**文档相关性**(选出最相关的 Doc_ID,回表查询)
> 5. **==回表==**:根据 Doc_ID,通过**聚簇索引**查询**原始数据表**,获取**完整行数据**。
>
> Doc_ID 即为原始数据表中,聚簇索引的 key。
> ^l3if7m
<br><br><br>
# ❓FAQ
### ❓ 为什么数据库通常采用 B+树实现索引,而不是 B 树?
B+ 树更适合**磁盘存储**,原因在于[^1]:
- (1)B+树**叶节点层**构成 "**有序双向链表**",支持**高效的==范围查询==**。⭐
- (2)B+树内部节点只存储 "**==索引==**",数据量相同时,相较于 B 树而言,**B+树的高度更低**,所需访问索引次数更少,故**磁盘 I/O 次数更少**。
###### 关于第一点的说明
B 树实现范围查询,只能通过**树遍历**,涉及多个节点的磁盘 I/O,效率低下。
###### 关于第二点的说明
**数据库索引**的设计通常遵循 **磁盘 I/O 优化原则**,而 **磁盘 I/O 的最小存取单位**即是 "**磁盘块**"(OS 文件系统通常规定为 4KB,例如 Linux 中)
因此在设计上,是希望 **一个==索引节点==的大小 ≈ 一个==磁盘块==的大小**,使得**每次访问一个索引节点时,只需要一次磁盘 I/O**。
B+树中**内部节点仅存储索引**,故相较于 B 树而言,**==单个节点上能存储的索引数量更多==**(要求节点大小受限于磁盘块大小),
使得**数据量相同**的情况下,**==B+树的高度更低==**,因此所需的**索引节点访问次数**更少,也即**磁盘 I/O 次数更少**。
例如,假设 **一个磁盘页的大小为 4KB**,每个索引键占 10B,每个数据记录(数据行)占 100B。
- 在 **B 树** 中,内部节点既要存储索引,也要存储数据(或数据指针),可能导致 **每个节点只能存储 30~40 个键值**。
- 在 **B+ 树** 中,内部节点 **仅存储索引**,不存储数据,因此可以 **存储 100~200 个键值**。
> [!example] 用于实现索引的 B+树示例
>
> 每**个索引节点的大小**设计为 **不"超过一个==磁盘块==的大小"**
>
> ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 索引.assets/IMG-MySQL 索引-BE12F975F616F696BEDB2A55A9E2DA51.png|808]]
>
> [!NOTE] 查找过程是先**从磁盘上读取索引到内存**,逐层**查找索引**,最后再通过索引找到目标数据,故 ==**树的高度**== 影响 **磁盘 I/O 操作的次数**。
<br><br>
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
# ♾️参考资料
- [换一个角度看 B+ 树](https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247502059&idx=1&sn=ccbee22bda8c3d6a98237be769a7c89c&scene=21#wechat_redirect)
- [为什么 MySQL 采用 B+ 树作为索引? | 小林coding](https://xiaolincoding.com/mysql/index/why_index_chose_bpuls_tree.html#_3%E3%80%81%E8%8C%83%E5%9B%B4%E6%9F%A5%E8%AF%A2)
- [索引常见面试题 | 小林coding](https://xiaolincoding.com/mysql/index/index_interview.html#%E6%8C%89%E5%AD%97%E6%AE%B5%E4%B8%AA%E6%95%B0%E5%88%86%E7%B1%BB)
# Footnotes
[^1]: [为什么 MySQL 采用 B+ 树作为索引? | 小林coding](https://xiaolincoding.com/mysql/index/why_index_chose_bpuls_tree.html#mysql-%E4%B8%AD%E7%9A%84-b-%E6%A0%91)
[^2]: [索引常见面试题 | 小林coding](https://xiaolincoding.com/mysql/index/index_interview.html#%E6%8C%89%E5%AD%97%E6%AE%B5%E4%B8%AA%E6%95%B0%E5%88%86%E7%B1%BB)