diff --git "a/content/docs/blog/SQL\344\270\200\350\241\214\346\200\216\344\271\210\345\255\230\345\202\250.md" "b/content/docs/blog/SQL\344\270\200\350\241\214\346\200\216\344\271\210\345\255\230\345\202\250.md" new file mode 100644 index 0000000..78f552f --- /dev/null +++ "b/content/docs/blog/SQL\344\270\200\350\241\214\346\200\216\344\271\210\345\255\230\345\202\250.md" @@ -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字节存储指向溢出页的地址。 diff --git a/static/pictures/mysql-row-storage.png b/static/pictures/mysql-row-storage.png new file mode 100644 index 0000000..3733bf5 Binary files /dev/null and b/static/pictures/mysql-row-storage.png differ