MySQL 数据存储结构
MySQL 数据存储完整结构(以最常用 InnoDB 引擎为主,顺带对比 MyISAM)
MySQL 本身是服务层,真正负责文件存储的是存储引擎,日常业务几乎都用 InnoDB,下面分:文件类型、文件内部结构、数据物理存储形态、一行数据长什么样。
一、MySQL 数据目录整体文件
默认数据目录datadir下,分三类文件:
1. 全局系统文件(所有库共用)
- ibdata1(共享表空间)开启共享表空间时,所有表数据、索引、undo 日志、数据字典都存在这里。
- 格式:二进制大文件,持续扩容,不会自动收缩
- ib_logfile0 /ib_logfile1(Redo 重做日志)崩溃恢复核心,循环写入,固定大小,二进制日志,存事务修改页的物理变更。
- ibtmp1(临时表空间)存放临时表、排序临时数据,重启自动清空。
- auto.cnf:MySQL 实例唯一 UUID,复制用。
- *.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 页操作,一页分为几部分:
- File Header 文件头(38 字节)页编号、校验和、上一页 / 下一页指针,形成双向链表
- Page Header 页头(56 字节)页内记录数量、空闲空间、删除链表、事务信息
- Infimum + Supremum 虚拟行页内最小、最大虚拟记录,用于索引二分查找边界
- User Records 用户数据区真正存储我们插入的行数据,按主键有序排列
- Free Space 空闲空间插入更新预留空位
- Page Directory 页目录稀疏索引,存几条记录的偏移,加速页内二分查找
页类型(不同页存不同内容)
- 数据页(B + 树叶子页):存完整行数据
- 索引页(B + 树非叶子页):只存主键 + 子页号,不存完整行
- Undo 页、系统页、事务页、LOB 大字段页
3. B + 树存储逻辑(InnoDB 主键索引)
每张表默认一个聚簇索引(主键):
- 叶子页:有序存储完整行数据,页之间双向链表串联
- 非叶子页:只存主键值 + 子页地址,用于快速路由到叶子页
- 二级索引(普通索引):叶子存「索引列 + 主键」,回表查询完整数据
4. 一条行记录(Row)内部长什么样(Compact 行格式,默认)
一行数据 =记录头 + 变长字段长度列表 + 标记位 + 字段真实值
拆解示例
表结构:
user(id INT PRIMARY KEY, name VARCHAR(20), age TINYINT)插入id=1, name="张三", age=20
- 记录头(5 字节)标记删除位、事务 ID、回滚指针、行链表指针、主键排序信息
- 变长字段长度列表name 是 VARCHAR,长度 2,存
0x02 - NULL 标记位:本记录无 NULL,0
- 真实字段存储
- id:4 字节 INT 二进制
- name:2 字节长度 + "张三" UTF8 二进制
- age:1 字节 TINYINT
特殊存储规则
- 变长字段(VARCHAR/TEXT)短文本直接存在行内;超长大文本溢出到独立溢出页(LOB 页),行内只存 20 字节指针
- 主键不允许 NULL,唯一标识行
- 隐藏列(每行自带,不可见)
- DB_TRX_ID:6 字节,最后修改该行的事务 ID
- DB_ROLL_PTR:7 字节,指向 undo 日志,实现 MVCC
- 无主键时自动生成 6 字节 DB_ROW_ID 隐藏主键
5. MVCC 多版本实现底层存储
更新数据不会直接覆盖原行:
- 修改时生成新行,新行的
DB_ROLL_PTR指向旧行 - 旧行数据存放在 Undo 日志页
- 不同事务根据事务 ID 可见性读取对应版本,实现无锁读
三、共享表空间 ibdata1 内部存储内容
如果关闭独立表空间,所有表数据、索引都存在 ibdata1,同时还存放全局数据:
- 所有表的数据字典(表、字段、索引元数据)
- Undo 回滚日志段
- 系统事务信息、MVCC 版本链
- 缓冲区变更持久化辅助信息
文件特点:只会增大,删除数据不会释放磁盘空间,只能通过OPTIMIZE TABLE重建回收。
四、Redo 日志 ib_logfile 文件作用(不属于表数据,但必不可少)
- 循环覆盖的固定大小二进制文件
- 事务提交先写 Redo,再刷盘数据页
- 数据库宕机重启,通过 Redo 恢复已提交未落盘的数据
- 只记录「页物理修改」,不记录 SQL 逻辑
五、直观对比:InnoDB .ibd vs MyISAM .MYD/.MYI
表格
| 特性 | InnoDB *.ibd | MyISAM MYD+MYI |
|---|---|---|
| 内容 | 数据 + 索引 + 事务元数据一体 | 数据、索引分离两个文件 |
| 事务 | 支持 MVCC、事务、崩溃安全 | 无事务,崩溃易损坏 |
| 主键 | 聚簇索引,数据按主键排序 | 堆表,无序存储 |
| 空间回收 | 删除仅标记,OPTIMIZE 回收 | 删除直接空洞,可复用 |
| 锁 | 行锁 | 表锁 |
六、肉眼直接看文件的直观特征
- 所有数据文件都是二进制文件,记事本打开全是乱码,不能直接编辑
- .ibd 文件大小随数据 / 索引增长,删除数据不会自动缩小
- ib_logfile 大小固定,数量 2 个,循环复用
- 库文件夹下只能放对应表的 ibd,不能跨库混用
- 8.0 彻底移除 frm 文件,表结构元数据统一存在共享数据字典表空间
七、补充:磁盘落盘完整流程(一条数据怎么存进文件)
- 执行 insert,数据写入内存 Buffer Pool 的数据页
- 写 Redo Log Buffer,刷入磁盘 ib_logfile(事务提交必刷)
- 后台线程(Page Cleaner)定期把内存脏页刷入 .ibd/ibdata1 磁盘文件
- 删除仅在页内标记删除位,不会立刻擦除磁盘数据,purge 线程后台清理旧版本 undo
ibd 一个文件怎么做到区分 → 表空间 (Tablespace) → 段 (Segment) → 区 (Extent) → 页 (Page) → 行 (Row)
一、核心结论先讲透
.ibd本身就是独立表空间文件(Single-table Tablespace),整个文件只属于一张表; 文件内部靠固定偏移、页头元数据、页类型、链表指针、位图管理五层标记,一层层拆分出:表空间 → 段 → 区 → 页 → 行。分层识别逻辑自上而下:
- 整个
.ibd= 1 个独立表空间(Tablespace)- 表空间按 1MB 切分连续区块 = 区(Extent)
- 多个区归属于同一个索引逻辑单元 = 段(Segment)
- 每个区固定 64 个 16KB 小块 = 页(Page)
- 每个页内部靠页头、记录头区分多条行(Row)
前置固定常量(所有划分的基准)
Page = 16KB = 16384 Byte Extent = 64 Page = 64 * 16KB = 1MB整个
.ibd文件从 0 字节开始,按 1MB 对齐切割,天然划分出一个个区。二、第一层:区分「表空间 Tablespace」—— 整个 ibd 就是一个表空间
怎么标识这是一个独立表空间?
- 文件最开头第 0 号页(File Header 页,0 号页 = 表空间头部页)页头固定存储:
SPACE_ID:表空间 ID,每个.ibd文件拥有唯一 ID- 表空间类型标记:区分共享 ibdata1 / 单表 ibd / 临时表空间
- 表空间总大小、已使用区数量、空闲区链表指针
- 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 号表空间头部页里维护两种链表:
FREE_EXTENTS:空闲区链表(整块 1MB 未分配)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 * 163842. 页头 File Header(每页前 38 字节)是区分核心
每页开头固定 38 字节元数据,包含关键标记:
PAGE_SPACE_ID:归属哪个表空间(校验是否属于当前 ibd)PAGE_OFFSET:当前页的页号 NPAGE_TYPE:页类型(数据页 / 索引页 / 段描述页 / LOB 页等)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 逐层定位逻辑:
- 根据表名找到对应
xxx.ibd文件 → 整个文件 = 1 个表空间(SPACE_ID 匹配)- 根据主键索引找到目标索引对应的段 SEGMENT_ID
- 遍历该段持有的所有区(1MB 区块),找到包含目标页的 Extent
- 通过页号计算 16KB 偏移,定位磁盘上具体 Page
- 读取页头确认是叶子数据页,通过 Page Directory 页目录找到行偏移
- 读取记录头,取出完整 Row 字段返回
八、极简分层识别总结表
表格
层级 划分依据核心标识 物理 / 逻辑划分 表空间 Tablespace ibd 文件独立绑定 SPACE_ID,0 号头部页标识 物理文件级 区 Extent 文件按 1MB 固定偏移切割,空闲区链表管理 物理连续区块 段 Segment 页内 SEGMENT_ID,相同 ID 归为一段 逻辑索引单元 页 Page 16KB 固定偏移,每页 PAGE_OFFSET 页号、PAGE_TYPE 最小 IO 物理单元 行 Row 页目录偏移 + 5 字节记录头链表 单页内逻辑数据单元
