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