在mysql中,存储数据最基本的单位是页,一页的大小是16K。在InnoDB中,有很多不同类型的页,存放数据的页称为数据页,我们来看看数据页的底层结构。
基本组成

| 名称 | 中文名 | 占用空间大小 | 简单描述 |
|---|---|---|---|
| File Header | 文件头部 | 38字节 | 页的一些通用信息 |
| Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
| Infimum + Supremum | 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
| User Records | 用户记录 | 不确定 | 实际存储的行记录内容 |
| Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
| Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
| File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
我们新建一张表,并插入几条数据
mysql> CREATE TABLE page_demo(
-> c1 INT,
-> c2 INT,
-> c3 VARCHAR(10000),
-> PRIMARY KEY (c1)
-> ) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd');
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
user records和free space
如何分配user records
一个页刚建好,user records这部分是不存在的,而free space这部分就是这个页所能存储的全部数据空间。当我们往数据页中插入第一条记录时,free space会分出需要的空间,被user records替换。随着记录越来越多,free space空间越来越小,user records空间越来越大。当free space的全部空间都被分配完了,这个页也就使用完了,需要申请新的页
记录头信息
- delete_mask
值为1,代表该记录被删除,所以你从表中删除一条记录时,这条记录并不会立刻从硬盘上被删除,只是被标记成删除。所有被删除的记录会组成一个链表,当有新的数据来的时候,可能不会分配新的空间,而是覆盖被删除的记录 - min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记 - n_owned
页中分组后组内最大记录会记录组内的记录条数 - heap_no
表示当前记录在当前页中的位置,记录会按照主键的大小进行排序 - record_type
记录类型,0是普通记录,1是b+树非叶子节点记录,2是最小记录,3是最大记录 - next_record
当前记录真实数据到下一条真实数据的地址偏移量。不是按照插入顺序,按照主键的顺序,infimum下一条是页中主键最小的,主键最大的下一条是supremum。当有记录被删除时,会指向下一条没有被删除的记录。 ==无论怎么对页中的数据进行增删改,InnoDb始终会维护一条记录的单链表,按照主键大小排序==
infimum和supremum
两条伪纪录,用于表示最小记录和最大纪录,存储引擎自动生成

page directory页目录
我们平时查字典的时候,如何快速找到想要查的字,要根据目录来,InnoDB也有类似的结构
- 将所有未被删除的记录划分成组
- 将组内主键最大的记录的记录头中n_owned属性值设为组内记录条数
- 在将组内最后一条记录的地址偏移提取出来按顺序存储到页的尾部,这就是页目录,页目录中的每一项成为槽

分组条数规则
最小记录分组只能有一条,最大记录分组可以有1-8条,其余在4-8条之间。
- 初始情况下只有最小记录和最大记录,属于两个分组
- 之后每插入一条,会按照主键顺序插入到相应的槽中,最大纪录n_owned值加一,直到组中的记录等于8
- 当组中数据等于8个时,再插入一条时,会将记录拆分成4条和5条的两个组,并新增一个槽
按照主键查找的流程

- 通过二分法确定该记录所在的槽,并找到该槽所在分组中最小的记录
- 遍历该槽所在的组中的记录
page header页面头部
用于记录本页存储了多少条记录,第一条记录的地址,页面中有多少个槽,占用固定的56字节
| 名称 | 占用空间大小 | 描述 |
|---|---|---|
| PAGE_N_DIR_SLOTS | 2字节 | 在页目录中的槽数量 |
| PAGE_HEAP_TOP | 2字节 | 还未使用的空间最小地址,也就是说从该地址之后就是Free Space |
| PAGE_N_HEAP | 2字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
| PAGE_FREE | 2字节 | 第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用) |
| PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 |
| PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 |
| PAGE_DIRECTION | 2字节 | 记录插入的方向 |
| PAGE_N_DIRECTION | 2字节 | 一个方向连续插入的记录数量 |
| PAGE_N_RECS | 2字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
| PAGE_MAX_TRX_ID | 8字节 | 修改当前页的最大事务ID,该值仅在二级索引中定义 |
| PAGE_LEVEL | 2字节 | 当前页在B+树中所处的层级 |
| PAGE_INDEX_ID | 8字节 | 索引ID,表示当前页属于哪个索引 |
- PAGE_DIRECTION
假如插入的一条记录主键值比上一条大,则插入方向是右边,反之是左边,用来表示最后一条记录的插入方向 - PAGE_N_DIRECTION
记录相同的插入方向的记录条数,若改变了方向,则置0
file header文件头部
针对所有的页通用,而page header只针对数据页,这部分会记录页面的通用信息,页编号,上页下页等等,占用38字节
| 名称 | 占用空间大小 | 描述 |
|---|---|---|
| FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 页的校验和(checksum值) |
| FIL_PAGE_OFFSET | 4字节 | 页号 |
| FIL_PAGE_PREV | 4字节 | 上一个页的页号 |
| FIL_PAGE_NEXT | 4字节 | 下一个页的页号 |
| FIL_PAGE_LSN | 8字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
| FIL_PAGE_TYPE | 2字节 | 该页的类型 |
| FIL_PAGE_FILE_FLUSH_LSN | 8字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
| FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
- FIL_PAGE_OFFSET
页号,给每个页编号,确定唯一的页 - FIL_PAGE_TYPE
记录页的类型,主要有
| 类型名称 | 十六进制 | 描述 |
|---|---|---|
| FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还没使用 |
| FIL_PAGE_UNDO_LOG | 0x0002 | Undo日志页 |
| FIL_PAGE_INODE | 0x0003 | 段信息节点 |
| FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Insert Buffer空闲列表 |
| FIL_PAGE_IBUF_BITMAP | 0x0005 | Insert Buffer位图 |
| FIL_PAGE_TYPE_SYS | 0x0006 | 系统页 |
| FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
| FIL_PAGE_TYPE_FSP_HDR | 0x0008 | 表空间头部信息 |
| FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
| FIL_PAGE_TYPE_BLOB | 0x000A | 溢出页 |
| FIL_PAGE_INDEX | 0x45BF | 索引页,也就是我们所说的数据页 |
- FIL_PAGE_PREV和FIL_PAGE_NEXT 页与页之间会组成双向链表,用这两个值来找到前一个和后一个页
file trailer文件尾部
在页的最尾部,用于检测页面的完整性,由8个字节组成
- 前四个字节是校验和
与file header校验和对应,如果两者不一致,说明同步出现错误 - 后四个字节代表页面最后被修改时对应的日志序列位置(LSN)