MySQL服务器上 存储引擎
负责对表中数据的读取和写入工作,不同存储引擎中 存放的格式
一般是不同的,甚至有的存储引擎(Memory)不用磁盘来存储数据。
- 页 (Page) 是磁盘和内存之间交互的
基本单位
,也就是说数据库管理存储空间的基本单位是页,数据库I/O操作的最小单位
是页 (InnoDB页默认大小16KB) - 区 (Extent) 是比页大一级的存储结构,在InnoDB存储引擎中 ,一个区会分配64个连续的页。因为InnoDB中页的默认大小为16KB,所以一个区的大小是1MB=64*16KB
- 段 (Segment) 由一个或多个区组成,段中不要求区与区之间是相邻的。段是
数据库中的分配单位
,不同类型的数据库对象以不同的段形式存在,当我们创建数据表、索引时,会创建对应的段。 - 表空间 (Tablespace) 是一个逻辑容器,在一个表空间中可以有一个或多个段,一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。
1_InnoDB页结构
数据库中的页可以 不在物理结构上相连
,只要通过 双向链表
关联即可。每个数据页中的记录会按照列 (主键或其他) 的顺序 (从小到大或者从大到小) 组成一个 单向链表
,每个数据页都会为存储在页内的记录生成一个 页目录
,在通过该列查找某条记录的时候可以在页目录中使用 二分法
快读定位到对应的槽,然后再遍历该槽对应的分组中的记录即可快速找到指定的纪录。
1.1.File Header
File Header 文件头部 (38字节) :描述页的通用信息。
- FIL_PAGE_OFFSET(4字节) : 页号(唯一)
- FIL_PAGE_TYPE(2字节) : 代表当前页的类型。
- FIL_PAGE_PREV(4字节) 和 FIL_PAGE_NEXT(4字节) : 分别代表本页的上一个和下一个页的页号。
通过建立一个双向链表把许多页都串联起来,保证这些页之间不需要是物理上的连续,而是逻辑上的连续
。 - FIL_PAGE_SPACE_OR_CHKSUM(4字节) : 当前页面的校验和(checksum)。为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况)
什么是校验和?
就是对于一个很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和。在比较两个很长的字节串之前,先比较这两个长字节串的校验和,如果校验和都不一样,则两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。
页的校验和的作用:
为了检测一个页是否完整,这时可以通过
文件尾的校验和
与文件头的校验和
做比对,如果两个值不相等则证明页的传输有问题,需要重新进行传输,否则认为页的传输已经完成。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
- FIL_PAGE_LSN(8字节) : 页面被最后修改时对应的日志序列位置
1.2.File Trailer
File Trailer 文件尾部(8字节):检验页是否完整
- 前4个字节代表页的校验和:这个部分是和File Header中的校验和相对应的。
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明同步过程出现了问题。
1.3.Free Space
Free Space 空闲记录:页中还没有被使用的记录
我们自己存储的记录会按照指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分
,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。
1.4.User Records
User Records 用户记录:存储行记录的内容
User Records中的这些记录按照指定的行格式
一条一条摆在User Records部分,相互之间形成单链表
。
1.5.Infimum+Supremum
Infimum+Supremum 最小和最大记录(26字节):虚拟的行记录
记录可以比大小,对于一条完整的记录来说,比较记录的大小就是比较主键的大小
。
比方说我们插入的4行记录的主键值分别是:1、2、3、4,这也就意味着这4条记录是从小到大依次递增。
InnoDB规定的最小记录与最大记录这两条记录的构造十分简单,都是由5字节大小的记录头信息
和8字节大小的一个固定的部分
组成的
这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分
1.6.Page Directory
Page Directory 页目录(56字节):存储用户记录的相对位置
在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录
,通过二分查找法
的方式进行检索,提升效率。
- 将所有的记录
分成几个组
,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。
InnoDB规定:
- 对于最小记录所在的分组只能有1条记录,
- 最大记录所在的分组拥有的记录条数只能在1~8条之间,
- 剩下的分组中记录的条数范围只能在是 4~8 条之间。
- 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为
n_owned
字段。
n_owned字段存在于行的记录头中
n_owned(4bit):页目录中每个组中最后一条记录的头信息中会存储该组一共有多少条记录
- 页目录用来存储
每组最后一条记录的地址偏移量
,这些地址偏移量会按照先后顺序存储
起来,每组的地址偏移量也被称之为槽(slot)
,每个槽相当于指针指向了不同组的最后一个记录。
分组的步骤:
- 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
- 每插入一条记录,都会从页目录中找到
主键值比本记录的主键值大并且差值最小的槽
,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。 - 在一个组中的记录数等于8个后再插入一条记录时,会
将组中的记录拆分成两个组
,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
举例:
页目录结构下查找记录
在一个数据页中查找指定主键值的记录的过程分为两步:
- 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
举例:
查找记录中主键值为6的记录:
因为各个槽代表的记录的主键值都是从小到大排序
的,所以我们可以使用二分法
来进行快速查找。
5个槽的编号分别是:0、1、2、3、4,所以初始情况下最低的槽就是low=0,最高的槽就是high=4。
- 计算中间槽的位置:(0+4)/2=2,所以查看槽2对应记录的主键值为8,又因为8 > 6,所以设置high=2,low保持不变。
- 重新计算中间槽的位置:(0+2)/2=1,所以查看槽1对应的主键值为4,又因为4 < 6,所以设置low=1,high保持不变。
- 因为high - low的值为1,所以确定主键值为6的记录在槽2对应的组中。此刻我们需要找到槽2中主键值最小的那条记录,然后沿着单向链表遍历槽2中的记录。
通过二分法确定主键值为6的记录在槽2对应的组中,定位槽2中最小的数据
怎么定位一个组中最小的记录呢?
别忘了各个槽都是挨着的,我们可以很轻易的拿到槽1对应的记录(主键值为4),该条记录的下一条记录就是槽2中主键值最小的记录 (记录之间通过单链表链接),该记录的主键值为5。
从槽2中最小的数据 (主键值为5的记录) 出发,遍历槽2中的各条记录,直到找到主键值为6的那条记录即可。
1.7.Page Header
Page Header 页面头部(56字节):页的状态信息
为了能得到一个数据页中存储的记录的状态信息
,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,这个部分占用固定的56个字节,专门存储各种状态信息。
2_InnoDB行格式
我们平时的数据以行为单位来向表中插入数据,这些记录在磁盘上的存放方式也被称为行格式
或者记录格式
。InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact
、Redundant
、Dynamic
和Compressed
行格式。
行格式操作
查看MySQL8的默认行格式:
SELECT @@innodb_default_row_format;
查看具体表的行格式:
SHOW TABLE STATUS like '表名';
指定行格式:
#创建表时指定行格式 CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称 #修改表的行格式 ALTER TABLE 表名 ROW_FORMAT=行格式名称
2.1.compact行格式
在MySQL 5.1版本中,默认设置为Compact行格式。一条完整的记录其实可以被分为记录的额外信息
和记录的真实数据
两大部分。
1.变长字段长度列表
变长字段长度列表:记录所有变长字段的真实数据占用的字节长度
MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、TEXT类型,BLOB类型,这些数据类型修饰列称为
变长字段
,变长字段中存储多少字节的数据不是固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。
注意:存储的变长长度和字段顺序相反
举例:
各个列都使用的是ASCII
字符集(每个字符只需要1个字节来进行编码)。
长度值按照列的逆序存放,所以最后变长字段长度列表的字节串用十六进制表示的效果就是:060408
行格式:
2.NULL值列表
NULL值列表:把可以为NULL的列统一管理起来,标记对应列的值是否为NULL
注意:
- 这里面存储的NULL值列表和字段
顺序相反
。 - 如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了。
为什么定义NULL值列表?
数据都是需要对齐的
,如果没有标注出来NULL值的位置,就有可能在查询数据的时候出现混乱。- 如果使用一个特定的符号放到相应的数据位表示空置的话,虽然能达到效果,但是这样很浪费
空间
,所以直接就在行数据得头部开辟出一块空间专门用来记录该行数据哪些是非空数据,哪些是空数据
- 二进制位的值为1时,代表该列的值为NULL。
- 二进制位的值为0时,代表该列的值不为NULL。
举例:
行格式:
3.记录头信息
- delete_mask(1bit):标记着当前记录是否被删除(1被删除,0没有删除)
被删除的记录为什么还在页中存储呢?你以为它删除了,可它还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后其他的记录在磁盘上需要重新排列,导致
性能
消耗。所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表
,在这个链表中的记录占用的空间称之为可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
- min_rec_mask(1bit):标识是否为最小的目录项记录(1最小记录,0不是最小记录)
- n_owned(4bit):页目录中每个组中最后一条记录的头信息中会存储该组一共有多少条记录
- heap_no(13bit):表示当前记录在本页中的位置
怎么不见heap_no值为0和1的记录呢?MySQL会自动给每个页里加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录。这两个伪记录一个代表
最小记录
,一个代表最大记录
。最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前。
- record_type(3bit):当前记录的类型
- 0:表示普通记录
- 1:表示目录项记录
- 2:表示最小记录
- 3:表示最大记录
- next_record(16bit):表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。
下一条记录:
下一条记录指得并不是按照我们插入顺序的下一条记录,而是
按照主键值由小到大的顺序
的下一条记录。而且规定Infimum记录(也就是最小记录)的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)。
4.记录的真实数据
记录的真实数据除了我们自己定义的列的数据以外,还会有三个隐藏列:
- row_id:一个表没有手动定义主键,则会选取一个Unique键作为主键,如果连Unique键都没有定义的话,则会为表默认添加一个名为row_id的隐藏列作为主键。所以row_id是在没有自定义主键以及Unique键的情况下才会存在的。
2.2.dynamic&compressed行格式
在MySQL 8.0中,默认行格式就是Dynamic,Dynamic、Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有分歧:
行溢出:
一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为行溢出。
- Compressed和Dynamic两种记录格式对于存放在BLOB中的数据采用了
完全的行溢出
的方式。 - Compact和Redundant两种格式会在记录的真实数据处存储一部分数据
Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩
,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。
2.3.redundant行格式
字段长度偏移列表与变长字段长度列表有两处不同:
- 少了“变长”两个字:Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。
- 多了“偏移”两个字:这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。
记录头信息与Compact行格式的记录头信息对比来看,有两处不同:
- Redundant行格式多了n_field和1byte_offs_flag这两个属性。
- n_fields:代表一行中列的数量,占用10位
- 1byte_offs_flags:定义了偏移列表占用的大小(1:1个字节,0:2个字节)
1byte_offs_flag的值是怎么选择的根据该条Redundant行格式记录的
真实数据占用的总大小
来判断的:
- 当记录的真实数据占用的字节数值不大于127(十六进制0x7F,二进制01111111)时,每个列对应的偏移量占用1个字节。
- 当记录的真实数据占用的字节数大于127,但不大于32767(十六进制0x7FFF,二进制0111111111111111)时,每个列对应的偏移量占用2个字节。
- 当记录的真实数据占用的字节数大于32767时,将部分记录存放到溢出页中,在本页中只保留前768个字节和20个字节的溢出页面地址。
- Redundant行格式没有record_type这个属性。
Redundant行格式中NULL值的处理:
因为Redundant行格式并没有NULL值列表,所以Redundant行格式在字段长度偏移列表中的各个列对应的偏移量处做了一些特殊处理 —— 将列对应的偏移量值的第一个比特位作为是否为NULL的依据
,该比特位也可以被称之为NULL比特位。也就是说在解析一条记录的某个列时,首先看一下该列对应的偏移量的NULL比特位是不是为1。如果为1,那么该列的值就是NULL,否则不是NULL。