MySQL 数据存储完整结构以最常用 InnoDB 引擎为主顺带对比 MyISAMMySQL 本身是服务层真正负责文件存储的是存储引擎日常业务几乎都用 InnoDB下面分文件类型、文件内部结构、数据物理存储形态、一行数据长什么样。一、MySQL 数据目录整体文件默认数据目录datadir下分三类文件1. 全局系统文件所有库共用ibdata1共享表空间开启共享表空间时所有表数据、索引、undo 日志、数据字典都存在这里。格式二进制大文件持续扩容不会自动收缩ib_logfile0 /ib_logfile1Redo 重做日志崩溃恢复核心循环写入固定大小二进制日志存事务修改页的物理变更。ibtmp1临时表空间存放临时表、排序临时数据重启自动清空。auto.cnfMySQL 实例唯一 UUID复制用。*.err错误日志文本文件。2. 每个数据库文件夹如 test_db/新建库会生成同名文件夹里面放单表独立文件innodb_file_per_tableON默认开启table_name.ibd单表数据 索引文件核心table_name.frm表结构定义文件5.7 及以前8.0 已废弃结构存在数据字典table_name.ibd.old表空间迁移备份文件3. MyISAM 老引擎文件不推荐生产一张表 3 个文件*.MYDMyISAM Data纯数据行文件*.MYIMyISAM Index索引树文件*.frm表结构二、InnoDB 核心.ibd 文件内部物理结构1. 整体层级从大到小ibd 文件→ 表空间 (Tablespace) → 段 (Segment) → 区 (Extent) → 页 (Page) → 行 (Row)关键单位尺寸页 Page16KBInnoDB 最小 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)插入id1, name张三, age20记录头5 字节标记删除位、事务 ID、回滚指针、行链表指针、主键排序信息变长字段长度列表name 是 VARCHAR长度 2存0x02NULL 标记位本记录无 NULL0真实字段存储id4 字节 INT 二进制name2 字节长度 张三 UTF8 二进制age1 字节 TINYINT特殊存储规则变长字段VARCHAR/TEXT短文本直接存在行内超长大文本溢出到独立溢出页LOB 页行内只存 20 字节指针主键不允许 NULL唯一标识行隐藏列每行自带不可见DB_TRX_ID6 字节最后修改该行的事务 IDDB_ROLL_PTR7 字节指向 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 *.ibdMyISAM MYDMYI内容数据 索引 事务元数据一体数据、索引分离两个文件事务支持 MVCC、事务、崩溃安全无事务崩溃易损坏主键聚簇索引数据按主键排序堆表无序存储空间回收删除仅标记OPTIMIZE 回收删除直接空洞可复用锁行锁表锁六、肉眼直接看文件的直观特征所有数据文件都是二进制文件记事本打开全是乱码不能直接编辑.ibd 文件大小随数据 / 索引增长删除数据不会自动缩小ib_logfile 大小固定数量 2 个循环复用库文件夹下只能放对应表的 ibd不能跨库混用8.0 彻底移除 frm 文件表结构元数据统一存在共享数据字典表空间七、补充磁盘落盘完整流程一条数据怎么存进文件执行 insert数据写入内存 Buffer Pool 的数据页写 Redo Log Buffer刷入磁盘 ib_logfile事务提交必刷后台线程Page Cleaner定期把内存脏页刷入 .ibd/ibdata1 磁盘文件删除仅在页内标记删除位不会立刻擦除磁盘数据purge 线程后台清理旧版本 undoibd 一个文件怎么做到区分 → 表空间 (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 ~ (n1)*1MB每个区内部固定 64 个连续页页编号连续 区 n 包含页号n*64 ~ n*6463靠什么管理空闲 / 已使用的区0 号表空间头部页里维护两种链表FREE_EXTENTS空闲区链表整块 1MB 未分配USED_EXTENTS已分配区链表 每个区头部会记录下一个空闲区、上一个空闲区指针形成双向链表。数据库分配空间时直接拿一整个空闲区给段使用删除数据回收整块空闲区放回空闲链表。一句话区分区文件按 1MB 固定长度切割靠头部页的区链表记录哪些区在用、哪些空闲。四、第三层区分「段 Segment」—— 逻辑索引单元多个区归属于一段区是物理连续块段是逻辑集合一张索引对应 2 个段B 树非叶子段 B 树叶子段一张表聚簇索引 二级索引会产生多段。核心问题怎么知道某个区属于哪个段1. 段描述符Segment Entry每个区的第一个页头部存放「段描述符」SEGMENT_ID段唯一编号段类型叶子段 / 非叶子段 /undo 段 / LOB 段该段持有多少个区、页数量该段的空闲页链表、满页链表同一个段的所有区段描述符里SEGMENT_ID完全相同InnoDB 就能把分散的多个区归为同一个逻辑段。2. 段的分配规则新建索引时先分配碎片页32 个零散页数据量小不占用完整 1MB 区碎片页用完后才一次性分配完整 Extent1MB 区加入该段一个段可以持有多个不连续的 1MB 区靠SEGMENT_ID关联起来。举例一张表主键聚簇索引段 AB 树非叶子节点段存放索引目录页段 BB 树叶子节点段存放真实数据页 两个段各自持有一批独立的区靠段 ID 区分归属。一句话区分段靠页内的 SEGMENT_ID 标识相同 ID 的所有区 / 页归为同一个逻辑段。五、第四层区分「页 Page」——16KB 最小 IO 单元靠页号 页头标记区分每个区 64 个页每页固定 16384 字节靠页编号 Page Number唯一标识。1. 页定位计算给定任意页号 N直接算出该页在 ibd 文件内的字节偏移offset N * 163842. 页头 File Header每页前 38 字节是区分核心每页开头固定 38 字节元数据包含关键标记PAGE_SPACE_ID归属哪个表空间校验是否属于当前 ibdPAGE_OFFSET当前页的页号 NPAGE_TYPE页类型数据页 / 索引页 / 段描述页 / LOB 页等PAGE_PREV / PAGE_NEXT同层页双向链表指针B 树叶子页串联3. 如何区分一页是数据页还是索引页靠PAGE_TYPE字段PAGE_INDEXB 树非叶子索引页只存主键无完整行PAGE_LEAFB 树叶子数据页存储完整行记录一句话区分页按 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 字段返回八、极简分层识别总结表表格层级划分依据核心标识物理 / 逻辑划分表空间 Tablespaceibd 文件独立绑定 SPACE_ID0 号头部页标识物理文件级区 Extent文件按 1MB 固定偏移切割空闲区链表管理物理连续区块段 Segment页内 SEGMENT_ID相同 ID 归为一段逻辑索引单元页 Page16KB 固定偏移每页 PAGE_OFFSET 页号、PAGE_TYPE最小 IO 物理单元行 Row页目录偏移 5 字节记录头链表单页内逻辑数据单元