Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions content/docs/blog/SQL一行怎么存储.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
title: "MySQL 一行记录是怎么存储的?"
weight: 7
type: docs
bookToC: true
---

参考:[小林 Coding](https://xiaolincoding.com/mysql/base/how_select.html)

- 一、数据存在哪个文件?
- 二、COMPACT行格式是什么样子?
- 三、InnoDB 页结构——数据真正存放的最小单元
- 四、B+Tree 索引结构与聚簇 / 非聚簇索引
- 五、行溢出后,MySQL怎么处理?
- 六、总结

# MySQL 一行记录是怎么存储的?

MySQL 数据由存储引擎管理,InnoDB 是默认存储引擎,所以主要以InnoDB引擎存储讨论。

# 一、数据存在哪个文件?

每个数据库对应一个目录(如 `/var/lib/mysql/my_test`),每张表对应一个 `.ibd` 文件(**表空间文件**)——表的数据、索引都存在这个文件里。

表空间的结构是分层的:

- **表空间(Tablespace)** → 由多个 **段(Segment)** 组成
- **段(Segment)** → 由多个 **区(Extent)** 组成
- **区(Extent)** → 由 64 个连续的 **页(Page)** 组成(每个页默认 16KB)
- **页(Page)** → 真正存放行记录的地方,是 InnoDB 管理磁盘的最小单位
- **行(Row)** → 我们要重点分析的一行记录的存储格式

#### 为什么要分层?

- InnoDB直接按**行**管理磁盘 I/O 太慢,所以用 **页(16KB)** 作为读写单位,一次 I/O 读入一整页数据到内存。
- 为了让 B+Tree 相邻的页在磁盘上也相邻,用 **区(1MB,64 个连续页)** 来分配空间,提升顺序 I/O 性能。

# 二、COMPACT行格式是什么样子?

![COMPACT 行格式示意图](/pictures/mysql-row-storage.png)

一行记录 = 「额外信息 + 真实数据」

MySQL除了存你写的字段,会存隐藏的内部信息[隐藏内部信息+真实字段数据]

**核心**:

**一行数据 = (内部头信息 + 变长字段 + NULL 列表) + 隐藏列 + 你的真实数据。**

## 1、4 个隐藏信息

### 1. 变长字段长度列表

- 比如 `varchar、text、blob` 这种**长度不固定**的字段
- MySQL 必须知道:这个字段占多少字节
- 所以会在头部,存一个「长度列表」

### 2. NULL 值列表

- 记录这一行里,**哪些字段是 NULL**
- 用 bit 位存,非常省空间
- 没有 NULL 的行,这部分可以没有

### 3. 记录头信息(固定)

- 标记这行是什么类型的记录
- 标记下一条数据在哪
- 标记是否删除
- 大小固定:**40 位(5 字节)**

### 4. 隐藏列(MySQL 强制加的)

如果你的表没有主键,InnoDB 会自动加三列:

- `DB_ROW_ID`:行 ID(6 字节)
- `DB_TRX_ID`:事务 ID(6 字节)
- `DB_ROLL_PTR`:回滚指针(7 字节)

## 2、真实数据部分

就是你建表时写的:

- int

- varchar

- char

- datetime


它们**紧跟在隐藏信息后面**。

# 三、InnoDB 页结构——数据真正存放的最小单元

InnoDB 读写磁盘的**最小单位是:页(Page)**。

默认大小:**16KB**。

所有数据、索引,都是按 “页” 来存、按 “页” 来加载。

**数据在页内是有序的**:按主键从小到大排序,方便快速查找。

**页与页之间是双向链表**:上一页 ↔ 下一页

---

## 1、一页是什么样的?

一页可以简单分成 **4 大块**:

### 1. 文件头部(File Header)

- 这一页的基本信息
- 上一页、下一页是谁(形成双向链表)
- 页类型(数据页?索引页?)

### 2. 页头部(Page Header)

- 这一页里有多少行记录
- 已经删了多少行
- 最后插入的位置等

### 3. **真正存数据的区域(核心)**

这里面放的就是我们**一行一行的数据**。

其结构:

- 已删除的行(垃圾数据)
- **用户记录(就是你存的真实数据)**
- 空闲空间(还没用到的空间)

数据行之间用**单向链表**串起来:

行 1 → 行 2 → 行 3 → …

### 4. 页尾部(File Trailer)

- 校验用
- 保证这一页写入时没有损坏

# 四、B+Tree 索引结构与聚簇 / 非聚簇索引

InnoDB 所有索引(包括主键)底层都是 **B+Tree** 结构

聚簇索引(主键)叶子节点就是数据,查询最快。

非聚簇索引叶子节点存的是主键,需要回表,除非是覆盖索引。

### B+Tree 为什么适合做数据库索引?

1. **多路平衡**:一个节点可以存很多键值,树的高度很低(通常 3-4 层),查询非常快。
2. 叶子节点有序且相连:
- 所有数据都在叶子节点,非叶子节点只存索引键和指针。
- 叶子节点之间是双向链表,支持高效的范围查询(`BETWEEN`、`ORDER BY`)。
3. **磁盘友好**:节点大小和页(16KB)对齐,一次 I/O 就能加载一个完整节点。

# 五、行溢出后,MySQL怎么处理?

MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB(16384字节)。

而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,一个数据页可能就存不了一条记录。这个时候就会发生行溢出:
InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中。

当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后**真实数据处用 20 字节存储指向溢出页的地址**,从而可以找到剩余数据所在的页。

# 六、总结

简要概括:一行的数据结构是头信息,隐藏列,真实数据。这一行会被存储到一个16KB的数据页中,在页内部每一行会按主键顺序排序链表,页与页之间按主键形成双向链表。而这些数据页会组成一个B+tree,就是聚簇索引,它的叶子节点存放完整的行数据。而非聚簇索引的叶子节点只存放索引列和主键,最后才回表提取完整行的数据。

行溢出的处理:当一行的数据内存大于16KB的时候,就会触发行溢出,InnoDB就会把一行中溢出的部分存放到溢出页中,而在原数据页中会留20字节存储指向溢出页的地址。
Binary file added static/pictures/mysql-row-storage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading