目录
一、基本存储结构-页
二、页的上层结构
三、页的内部结构
3.1 文件头与文件尾
3.2 记录部分
3.3 页头与页目录
四、记录的行格式
4.1 Compact行格式
4.1.1 变长字段长度列表
4.1.2 NULL值列表
4.1.3 记录头信息
4.1.4 真实信息
4.2 Dynamic和Compressed行格式
4.3 Redundant 行格式(5.0之前的格式,略)
五、区/段/碎片区
5.1 为什么引入区?
5.2 为什么引入段?
5.3 为什么引入碎片区?
InnoDB将数据划分为若干页,每个页默认大小16KB。
页是磁盘和内存交互的基本单位,每次IO最少读取16KB的内容到内存。也就是说,IO的基本单位是页。一个页中可以存储多个行记录。
页之间可以不在物理结构上相连,通过双向链表相关联。页内的记录按主键大小排序构成单向链表。
行->页->区->段->表空间
区在文件系统中是连续分配的空间,一个区等于64个页,大小:64*16K=1MB
段中不要求区之间是相邻的,不同类型的数据库对象以不同段的形式存在。例如数据表段、索引段。
表空间可存储一个或多个段,数据库由多个表空间组成,空间可划分为系统表空间(只有一个,额外记录系统信息)、用户表空间、临时表空间等。
文件头(38字节)的作用:
描述各种页的通用信息。(比如页的编号、类型、校验和、上一页、下一页,页面被最后修改时对应的日志序列位置)
文件尾(8字节)的作用:
前4个字节代表页的校验和:这部分和文件头中的校验和相对应。
后4个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明同步过程出现了问题。
页的主要作用是存储记录,所以“最大和最小记录”和“用户记录”部分占了页结构的主要空间。
最开始没有用户记录,全是空闲空间。我们每加一条记录,就会把空闲空间分出来一部分给用户记录,用户记录之间按主键大小用单链表连接。如果空闲空间用完了,就申请新的页。
页头:
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,这个部分占用固定的56个字节。
页目录:
在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索,提升效率。
在MySQL 5.1版本中,默认设置为Compact行格式。一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分。
MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、TEXT类型,BLOB类型,这些数据类型修饰列称为变长字段,变长字段中存储多少字节的数据不是固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。
Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值列表中。如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了。
之所以要存储NULL是因为数据都是需要对齐的,如果没有标注出来NULL值的位置,就有可能在查询数据的时候出现混乱。
包含如下属性。
除了真正的信息,还有三个隐藏列:
行ID:没有指定主键和Unique时存在,唯一标识一条记录。
事务ID
回滚指针
一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为行溢出。
在MySQL 8.0中,默认行格式就是Dynamic,Dynamic、Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有分歧:
Compressed和Dynamic两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式。如图,在数据页中只存放20个字节的指针(溢出页的地址),实际的数据都存放在溢出页中。
Compact格式会在记录的真实数据处存储一部分数据(存放768个前缀字节)。
Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。
我们知道页之间用双向链表相连,但页之间的物理位置可能相距很远。假如进行范围查询,我们顺着双向链表去查找,相当于随机IO,速度非常慢。为了尽可能让页按顺序存储,读取时采用顺序IO,我们引入了区的概念:一个区就是物理位置上连续的64个页。表中数据量大时,为索引分配空间就以区为单位分配,而非页。
对于范围查询,就是对B+树叶子节点的记录扫描。如果不区分叶子节点和非叶子节点,统统放进同一个区的话,查询效果就会降低。所以我们把叶子节点和非叶子节点分开,放到不同的区。那么相同类型的区就形成了段。例如一个索引会生成数据段、索引段等。
一个区默认占用1M的空间,一个索引生成两个段,而段以区分配空间。但记录较少的时候我们没必要申请这么多空间,所以就引入了碎片区,碎片区中的页所属的段不确定。碎片区直属于表空间,不属于任何一个段。
刚开始插数据时,段是从某个碎片区以单个页面分配存储空间的,直到某个段占用了32个碎片区页面后才会申请以完整的区为单位来分配空间。
所以区可分为四类:
1.空闲区 2.有剩余空间的碎片区 3.无剩余空间的碎片区 4.属于某个段的区