根据小孩子4919的《MySQL是怎样运行的:从根儿上理解MySQL》总结的
一、存储的结构
InnoDB存储引擎存储记录需要从宏观到微观的方式去看,首先是按照B+树的方式去存储数据的,叶子节点存放的是真实的数据记录,也称作==用户记录==,而在非叶子节点存放的是数据页的信息,称作==目录项记录==。
整体的结构如上面所示,再微观一点的看呢,每一个节点存储的都是一个页,每一个页都存放了一些数据,如下图。同一高度的相邻页都是按照双向链表连接的,页中的相邻数据则是按照单项链表连接的。==页组成的双向链表==可以方便快速找到上一页和下一页,==数据组成的单向链表==可以通过分组和插槽进行二分法快速定位。
每一个页到达一定规模存储不下的时候,都会分裂成新的页,同时在它的父节点处加一条页的数据。同理,目录项记录变多也会导致页的分裂,在父节点添加数据.....==目录项记录==存储以下内容,页当中最小的主键值key(方便后续查找),页号page_no。
如果是按照主键进行排序存储的话,页当中存储的就是完整的记录,这个索引也被称作==聚簇索引==,对于那些也想有序存储以便于快速查找的列,也可以建立索引,但是页中记录只存储==索引列的值==,==主键值==,这时要查询就是要先查询索引列的值,然后匹配主键列的值,再根据主键==回表查询==,这种方式的索引叫==非聚簇索引==。
二、数据页的结构
一个完整的表的存储方式就是按照B+树索引方式存储(这只是InnoDB的一种存储方式),数据页里也包含几个部分,一页默认是16KB,7个部分,图如下:
各部分组成及作用如下表:
| 名称 | 中文名 | 占用空间大小 | 简单描述 |
|---|---|---|---|
| File Header | 文件头部 | 38字节 | 页的一些通用信息 |
| Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
| Infimum + Supremum | 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
| User Record | 用户记录 | 不确定 | 实际存储的行记录内容 |
| Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
| Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
| File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
各部分更加详细的内容如下:
- Free Space:表示页当中未使用的空间
- User Record:记录的数据是存储在这个部分,最开始没有,每放入一条记录会从Free Space中分配出来,页空间用完了会申请新页
- Infimum和Supremum:规定这两个是页中最小值和最大值
页中的数据是按照链表的方式指向的,按照索引的顺序指向,从一条记录指向下一条记录。
删除一条记录,并不是直接把这条记录从页中删除,而是给它标记成已删除,记录还在,但指向不在了,这条记录会放入一个叫垃圾链表的地方,这样如果后面恢复也可以很快恢复,不会经常的改变索引的结构。
- Page Directory:页中的数据记录会进行分组,每组都包含一些数据,组中==最后一条记录==与页面中第0个字节的距离(地址偏移量)叫做==槽==。如下图,槽对应着每组最大的记录,这样查找时可以通过二分法去找到位于中间的槽,进而找到槽对应的最大记录的主键与期望查找的主键比较,然后再查找。
- Page Header:存储了页的一些信息
| 名称 | 占用空间大小 | 描述 |
|---|---|---|
| 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_BTR_SEG_LEAF | 10字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
| PAGE_BTR_SEG_TOP | 10字节 | B+树非叶子段的头部信息,仅在B+树的Root页定义 |
- File Header:记录比较通用的一些文件头信息
| 名称 | 占用空间大小 | 描述 |
|---|---|---|
| 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_TYP | 2字节 | 该页的类型 |
| FIL_PAGE_FILE_FLUSH_LSN | 8字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
| FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
==FIL_PAGE_PREV==和==FIL_PAGE_NEXT==记录的是上一个页号和下一个页号,所以这些数据页构成了一个大的链表。
- File Trailer:文件的尾信息,由8个字节组成,前4个字节代表页的校验和,如果修改记录的时候突然断电,避免出现同步异常,同步之前先计算个校验和,存储到File Header中,存储于磁盘中,等到同步的时候,会从File Trailor中比较校验和,不同步就是中间出错了。
三、记录的结构
InnoDB页存储的记录是有几种格式的:==Compact行格式,Redundant行格式,Dynamic行格式,Compressed行格式==。这里主要是Compress行格式的结构:
1、记录的额外信息
- 变长字段长度列表:有一些可变长字段如果按真实存储会很麻烦,所以存在变长字段长度列表中,变长字段长度列表==只存储非Null列的长度==,而且是==逆序存放==。
- Null值长度列表:对于Null值会存储到这个部分,先记录能存放Null值的列,然后同样是按照Null值字段==逆序存放==。==二进制值为1,则列值为Null,二进制值为0,则列值不为Null,这些字段的值按整数个字节存放。==
- 记录头信息:用于存放该记录的一些其他信息
| 名称 | 大小(单位:bit) | 描述 |
|---|---|---|
| 预留位1 | 1 | 没有使用 |
| 预留位2 | 1 | 没有使用 |
| delete_mask | 1 | 标记该记录是否被删除 |
| min_rec_mask | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
| n_owned | 4 | 表示当前记录拥有的记录数 |
| heap_no | 13 | 表示当前记录在记录堆的位置信息 |
| record_type | 3 | 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录 |
| next_record | 16 | 表示下一条记录的相对位置 |
2、记录的真实数据
MySQL会给记录加上一些隐藏列:
| 列名 | 是否必须 | 占用空间 | 描述 |
|---|---|---|---|
| row_id | 否 | 6字节 | 行ID,唯一标识一条记录 |
| transaction_id | 是 | 6字节 | 事务ID |
| roll_pointer | 是 | 7字节 | 回滚指针 |
InnoDB表对主键的==生成策略==:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。