MySQL 数据存储结构

📅 2026/7/8 1:48:53 👁️ 阅读次数 📝 编程学习
MySQL 数据存储结构

MySQL 数据存储完整结构(以最常用 InnoDB 引擎为主,顺带对比 MyISAM)

MySQL 本身是服务层,真正负责文件存储的是存储引擎,日常业务几乎都用 InnoDB,下面分:文件类型、文件内部结构、数据物理存储形态、一行数据长什么样。

一、MySQL 数据目录整体文件

默认数据目录datadir下,分三类文件:

1. 全局系统文件(所有库共用)

  1. ibdata1(共享表空间)开启共享表空间时,所有表数据、索引、undo 日志、数据字典都存在这里。
    • 格式:二进制大文件,持续扩容,不会自动收缩
  2. ib_logfile0 /ib_logfile1(Redo 重做日志)崩溃恢复核心,循环写入,固定大小,二进制日志,存事务修改页的物理变更。
  3. ibtmp1(临时表空间)存放临时表、排序临时数据,重启自动清空。
  4. auto.cnf:MySQL 实例唯一 UUID,复制用。
  5. *.err:错误日志文本文件。

2. 每个数据库文件夹(如 test_db/)

新建库会生成同名文件夹,里面放单表独立文件(innodb_file_per_table=ON默认开启):

  • table_name.ibd单表数据 + 索引文件(核心)
  • table_name.frm:表结构定义文件(5.7 及以前,8.0 已废弃,结构存在数据字典)
  • table_name.ibd.old:表空间迁移备份文件

3. MyISAM 老引擎文件(不推荐生产)

一张表 3 个文件:

  • *.MYD:MyISAM Data,纯数据行文件
  • *.MYI:MyISAM Index,索引树文件
  • *.frm:表结构

二、InnoDB 核心:.ibd 文件内部物理结构

1. 整体层级(从大到小)

ibd 文件→ 表空间 (Tablespace) → 段 (Segment) → 区 (Extent) → 页 (Page) → 行 (Row)

关键单位尺寸

  • 页 Page:16KB(InnoDB 最小 IO 单位,固定不可改)
  • 区 Extent:连续 64 个页 = 1MB,批量分配空间
  • 段 Segment:一张索引对应 2 个段(叶子段 + 非叶子段)

2. Page 页结构(最小读写单元)

所有读写都是按 16KB 页操作,一页分为几部分:

  1. File Header 文件头(38 字节)页编号、校验和、上一页 / 下一页指针,形成双向链表
  2. Page Header 页头(56 字节)页内记录数量、空闲空间、删除链表、事务信息
  3. Infimum + Supremum 虚拟行页内最小、最大虚拟记录,用于索引二分查找边界
  4. User Records 用户数据区真正存储我们插入的行数据,按主键有序排列
  5. Free Space 空闲空间插入更新预留空位
  6. Page Directory 页目录稀疏索引,存几条记录的偏移,加速页内二分查找

页类型(不同页存不同内容)

  • 数据页(B + 树叶子页):存完整行数据
  • 索引页(B + 树非叶子页):只存主键 + 子页号,不存完整行
  • Undo 页、系统页、事务页、LOB 大字段页

3. B + 树存储逻辑(InnoDB 主键索引)

每张表默认一个聚簇索引(主键):

  1. 叶子页:有序存储完整行数据,页之间双向链表串联
  2. 非叶子页:只存主键值 + 子页地址,用于快速路由到叶子页
  3. 二级索引(普通索引):叶子存「索引列 + 主键」,回表查询完整数据

4. 一条行记录(Row)内部长什么样(Compact 行格式,默认)

一行数据 =记录头 + 变长字段长度列表 + 标记位 + 字段真实值

拆解示例

表结构:

user(id INT PRIMARY KEY, name VARCHAR(20), age TINYINT)

插入id=1, name="张三", age=20

  1. 记录头(5 字节)标记删除位、事务 ID、回滚指针、行链表指针、主键排序信息
  2. 变长字段长度列表name 是 VARCHAR,长度 2,存0x02
  3. NULL 标记位:本记录无 NULL,0
  4. 真实字段存储
    • id:4 字节 INT 二进制
    • name:2 字节长度 + "张三" UTF8 二进制
    • age:1 字节 TINYINT

特殊存储规则

  1. 变长字段(VARCHAR/TEXT)短文本直接存在行内;超长大文本溢出到独立溢出页(LOB 页),行内只存 20 字节指针
  2. 主键不允许 NULL,唯一标识行
  3. 隐藏列(每行自带,不可见)
    • DB_TRX_ID:6 字节,最后修改该行的事务 ID
    • DB_ROLL_PTR:7 字节,指向 undo 日志,实现 MVCC
    • 无主键时自动生成 6 字节 DB_ROW_ID 隐藏主键

5. MVCC 多版本实现底层存储

更新数据不会直接覆盖原行:

  1. 修改时生成新行,新行的DB_ROLL_PTR指向旧行
  2. 旧行数据存放在 Undo 日志页
  3. 不同事务根据事务 ID 可见性读取对应版本,实现无锁读

三、共享表空间 ibdata1 内部存储内容

如果关闭独立表空间,所有表数据、索引都存在 ibdata1,同时还存放全局数据:

  1. 所有表的数据字典(表、字段、索引元数据)
  2. Undo 回滚日志段
  3. 系统事务信息、MVCC 版本链
  4. 缓冲区变更持久化辅助信息

文件特点:只会增大,删除数据不会释放磁盘空间,只能通过OPTIMIZE TABLE重建回收。

四、Redo 日志 ib_logfile 文件作用(不属于表数据,但必不可少)

  1. 循环覆盖的固定大小二进制文件
  2. 事务提交先写 Redo,再刷盘数据页
  3. 数据库宕机重启,通过 Redo 恢复已提交未落盘的数据
  4. 只记录「页物理修改」,不记录 SQL 逻辑

五、直观对比:InnoDB .ibd vs MyISAM .MYD/.MYI

表格

特性InnoDB *.ibdMyISAM MYD+MYI
内容数据 + 索引 + 事务元数据一体数据、索引分离两个文件
事务支持 MVCC、事务、崩溃安全无事务,崩溃易损坏
主键聚簇索引,数据按主键排序堆表,无序存储
空间回收删除仅标记,OPTIMIZE 回收删除直接空洞,可复用
行锁表锁

六、肉眼直接看文件的直观特征

  1. 所有数据文件都是二进制文件,记事本打开全是乱码,不能直接编辑
  2. .ibd 文件大小随数据 / 索引增长,删除数据不会自动缩小
  3. ib_logfile 大小固定,数量 2 个,循环复用
  4. 库文件夹下只能放对应表的 ibd,不能跨库混用
  5. 8.0 彻底移除 frm 文件,表结构元数据统一存在共享数据字典表空间

七、补充:磁盘落盘完整流程(一条数据怎么存进文件)

  1. 执行 insert,数据写入内存 Buffer Pool 的数据页
  2. 写 Redo Log Buffer,刷入磁盘 ib_logfile(事务提交必刷)
  3. 后台线程(Page Cleaner)定期把内存脏页刷入 .ibd/ibdata1 磁盘文件
  4. 删除仅在页内标记删除位,不会立刻擦除磁盘数据,purge 线程后台清理旧版本 undo

ibd 一个文件怎么做到区分 → 表空间 (Tablespace) → 段 (Segment) → 区 (Extent) → 页 (Page) → 行 (Row)

一、核心结论先讲透

.ibd本身就是独立表空间文件(Single-table Tablespace),整个文件只属于一张表; 文件内部靠固定偏移、页头元数据、页类型、链表指针、位图管理五层标记,一层层拆分出:表空间 → 段 → 区 → 页 → 行。

分层识别逻辑自上而下:

  1. 整个.ibd= 1 个独立表空间(Tablespace)
  2. 表空间按 1MB 切分连续区块 = 区(Extent)
  3. 多个区归属于同一个索引逻辑单元 = 段(Segment)
  4. 每个区固定 64 个 16KB 小块 = 页(Page)
  5. 每个页内部靠页头、记录头区分多条行(Row)

前置固定常量(所有划分的基准)

Page = 16KB = 16384 Byte Extent = 64 Page = 64 * 16KB = 1MB

整个.ibd文件从 0 字节开始,按 1MB 对齐切割,天然划分出一个个区。

二、第一层:区分「表空间 Tablespace」—— 整个 ibd 就是一个表空间

怎么标识这是一个独立表空间?

  1. 文件最开头第 0 号页(File Header 页,0 号页 = 表空间头部页)页头固定存储:
    • SPACE_ID:表空间 ID,每个.ibd文件拥有唯一 ID
    • 表空间类型标记:区分共享 ibdata1 / 单表 ibd / 临时表空间
    • 表空间总大小、已使用区数量、空闲区链表指针
  2. MySQL 元数据字典绑定关系 数据字典里记录table_name ↔ space_id ↔ xxx.ibd 文件路径,数据库能通过表直接定位对应 ibd 文件,一个 ibd 只对应一张表。

一句话区分表空间:一个.ibd= 一个独立 Tablespace,靠文件头部 0 号页的 SPACE_ID 标识归属。

三、第二层:区分「区 Extent」—— 按 1MB 物理边界切分

不需要额外标记,纯物理地址偏移划分,计算方式:

  • 第 0 个区:文件字节 0 ~ 1MB
  • 第 1 个区:1MB ~ 2MB
  • 第 n 个区:n*1MB ~ (n+1)*1MB

每个区内部固定 64 个连续页,页编号连续: 区 n 包含页号:n*64 ~ n*64+63

靠什么管理空闲 / 已使用的区?

0 号表空间头部页里维护两种链表:

  1. FREE_EXTENTS:空闲区链表(整块 1MB 未分配)
  2. USED_EXTENTS:已分配区链表 每个区头部会记录:下一个空闲区、上一个空闲区指针,形成双向链表。

数据库分配空间时,直接拿一整个空闲区给段使用;删除数据回收整块空闲区放回空闲链表。

一句话区分区:文件按 1MB 固定长度切割,靠头部页的区链表记录哪些区在用、哪些空闲。

四、第三层:区分「段 Segment」—— 逻辑索引单元,多个区归属于一段

区是物理连续块,段是逻辑集合:一张索引对应 2 个段(B + 树非叶子段 + B + 树叶子段),一张表聚簇索引 + 二级索引会产生多段。

核心问题:怎么知道某个区属于哪个段?

1. 段描述符(Segment Entry)

每个区的第一个页头部,存放「段描述符」:

  • SEGMENT_ID:段唯一编号
  • 段类型:叶子段 / 非叶子段 /undo 段 / LOB 段
  • 该段持有多少个区、页数量
  • 该段的空闲页链表、满页链表

同一个段的所有区,段描述符里SEGMENT_ID完全相同,InnoDB 就能把分散的多个区归为同一个逻辑段。

2. 段的分配规则

  • 新建索引时,先分配碎片页(32 个零散页),数据量小不占用完整 1MB 区;
  • 碎片页用完后,才一次性分配完整 Extent(1MB 区)加入该段;
  • 一个段可以持有多个不连续的 1MB 区,靠SEGMENT_ID关联起来。

举例

一张表主键聚簇索引:

  • 段 A:B + 树非叶子节点段(存放索引目录页)
  • 段 B:B + 树叶子节点段(存放真实数据页) 两个段各自持有一批独立的区,靠段 ID 区分归属。

一句话区分段:靠页内的 SEGMENT_ID 标识,相同 ID 的所有区 / 页归为同一个逻辑段。

五、第四层:区分「页 Page」——16KB 最小 IO 单元,靠页号 + 页头标记区分

每个区 64 个页,每页固定 16384 字节,靠页编号 Page Number唯一标识。

1. 页定位计算

给定任意页号 N,直接算出该页在 ibd 文件内的字节偏移:offset = N * 16384

2. 页头 File Header(每页前 38 字节)是区分核心

每页开头固定 38 字节元数据,包含关键标记:

  1. PAGE_SPACE_ID:归属哪个表空间(校验是否属于当前 ibd)
  2. PAGE_OFFSET:当前页的页号 N
  3. PAGE_TYPE:页类型(数据页 / 索引页 / 段描述页 / LOB 页等)
  4. PAGE_PREV / PAGE_NEXT:同层页双向链表指针(B + 树叶子页串联)

3. 如何区分一页是数据页还是索引页?

PAGE_TYPE字段:

  • PAGE_INDEX:B + 树非叶子索引页(只存主键,无完整行)
  • PAGE_LEAF:B + 树叶子数据页(存储完整行记录)

一句话区分页:按 16KB 固定偏移分割,每页头部存页号、页类型,唯一识别一页。

六、第五层:区分「行 Row」—— 单页内部靠记录头、偏移目录区分多条数据

一页 16KB 内会存储多条行记录,靠两套机制区分每一行:

1. 页内 Page Directory(页目录)

页尾部维护稀疏目录,存储多条记录的页内偏移地址,类似页内索引; 通过目录可以快速定位任意一条记录在 16KB 页中的起始字节位置。

2. 每条记录开头 5 字节 Record Header(记录头)

每一行数据最前面固定 5 字节头部,区分行状态:

  • 删除标记(是否被逻辑删除)
  • 下一条记录的偏移(页内单向链表,所有有效行串成有序链表)
  • 事务 ID、回滚指针标记(MVCC 多版本)

3. 虚拟边界行

页内固定存在两条虚拟行:Infimum(最小边界)、Supremum(最大边界),作为行链表首尾,方便二分查找。

读取流程: 页目录 → 找到记录偏移 → 定位 5 字节记录头 → 读取完整一行字段。

一句话区分行:页尾部目录记录每行偏移,每行开头 5 字节记录头串联所有有效数据行。

七、完整一层一层定位演示(举个实例)

查询表中一条数据,InnoDB 逐层定位逻辑:

  1. 根据表名找到对应xxx.ibd文件 → 整个文件 = 1 个表空间(SPACE_ID 匹配)
  2. 根据主键索引找到目标索引对应的段 SEGMENT_ID
  3. 遍历该段持有的所有区(1MB 区块),找到包含目标页的 Extent
  4. 通过页号计算 16KB 偏移,定位磁盘上具体 Page
  5. 读取页头确认是叶子数据页,通过 Page Directory 页目录找到行偏移
  6. 读取记录头,取出完整 Row 字段返回

八、极简分层识别总结表

表格

层级划分依据核心标识物理 / 逻辑划分
表空间 Tablespaceibd 文件独立绑定 SPACE_ID,0 号头部页标识物理文件级
区 Extent文件按 1MB 固定偏移切割,空闲区链表管理物理连续区块
段 Segment页内 SEGMENT_ID,相同 ID 归为一段逻辑索引单元
页 Page16KB 固定偏移,每页 PAGE_OFFSET 页号、PAGE_TYPE最小 IO 物理单元
行 Row页目录偏移 + 5 字节记录头链表单页内逻辑数据单元