详解[MySQL面试] InnoDB 一行记录的存储逻辑

0 阅读5分钟

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

目录

  • 一、数据存在哪个文件?
  • 二、COMPACT行格式是什么样子?
  • 三、InnoDB 页结构——数据真正存放的最小单元
  • 四、B+Tree 索引结构与聚簇 / 非聚簇索引
  • 五、行溢出后,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行格式是什么样子?

image-20260306010305543.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. 叶子节点有序且相连:

    • 所有数据都在叶子节点,非叶子节点只存索引键和指针。
    • 叶子节点之间是双向链表,支持高效的范围查询(BETWEENORDER BY)。
  3. 磁盘友好:节点大小和页(16KB)对齐,一次 I/O 就能加载一个完整节点。

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

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

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

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

五、总结

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

行溢出的处理:当一行的数据内存大于16KB的时候,就会触发行溢出,InnoDB就会把一行中溢出的部分存放到溢出页中,而在原数据页中会留20字节存储指向溢出页的地址。

参考 MySQL小林coding