「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」
varchar变成字符串如何存储
在MySQL中有一些字段的长度是变长的,比如我们经常用的varchar字符串类型,实际存放的字符串长度是不固定可变的。如果一行数据中有字段类型为varchar(10),char(2),char(3),存放的数据为:hello ni hao,第一个字段的长度不固定,后面两个字段的长度都是固定的1个字符。为了解决这种变长字段的读取引入变长字段的长度列表,解决一行数据的读取问题。 在存储每一行数据的时候,都保存一下变长字段的长度列表,在存储hello ni hao这行数据的时候,额外带上一些附加信息,在前面带上变长字段的长度列表。hello的长度是5,十六进制是:0x05,所以在磁盘文件存储的时候就类似下面的格式:
0x05 null值列表 数据头 hello ni hao
在读取变长字段的时候,首先会知道表里的字段类型是varchar(10)、char(2)、char(3),那么读取第一个值的时候,知道第一个字段是变长的,则先读取变长字段的长度0x05这个十六进制数字,然后读取第一行前5个字符串为hello,第二个字段为定长:2,则读取两位:ni,第三个字段为定长:3,则读取三位:hao,这样一行一行依次读取出数据。
多个变长字段又是如何存储
当一行数据有:varchar(10),varchar(5),varchar(20),char(2),char(3) 这5个字段时,存在三个变长字段,那么在开头的时候会存储几个变长字段的长度,但是这存储的时候是逆序存储,先存放varchar(20)这个字段的长度,然后在存放varchar(5),最后存放varchar(10)的长度,假设数据为:hello java mysql ni hao,那么在磁盘存储的时候类似下面的格式:
0x14 0x05 0x0a null值列表 头字段 hello java mysql ni hao
NULL值如何存储
当一行数据可能字段是NULL的时候,如果按照"NULL"字符串来存储的时候,会比较浪费空间,并且可能会跟真正的NULL字符串冲突,所以对所有的NULL值不通过字符串来磁盘存储,而是通过二进制bit位来存储,如果一行数据有多个字段都是NULL,那么这多个字段的NULL就会以bit的形式存放在NULL值列表中。 如果某张表有4个字段都允许为NULL,实际的数据为:不为NULL NULL 不为NULL NULL,则4个bit为:1010,但是实际放在NULL值列表的时候,他是按逆序放的,所以在NULL值列表里,放的是:0101,其实在实际存储NULL值列表存放的时候,不会仅仅是4个bit,一般起码是:8个bit的倍数,如果不足8位则最高位补0,则实际存放的为:00000101 在读取数据的时候,会把变长字段和NULL值列表读取出来,然后根据表结构分析有几个变长字段,哪几个变长字段是NULL,此时就可以从变长字段列表中解析出不为NULL的变长字段的值长度,也知道哪几个字段是NULL的,此时根据这些信息,就可以从实际的列值存储区域里,把你每个字段的值读取出来。 如果是变长字段则按照实际的值长度读取,如果是NULL,则知道是NULL,没有值存储,如果是定长字段,就按照定长长度来读取,这样的话就可以将一行数据读取出来。
40个bit数据头
在真实数据存储的时候还有个40个bit位的数据头,用这个数据头来秒杀这行数据信息。
- 第1位和第2位:预留位,没有任何含义
- 第3位:delete_mask标记,用来标记这行数据是否被删除,MySQL删除一行数据的时候,未必是立马将数据从磁盘清理掉,而是先搞一个标记位来标记删除
- 第4位:min_rec_mask,B+树里每一层的非叶子节点里的最小值都有这个标记
- 第5-8位(4个):n_owned,记录了一个记录数
- 第9-21位(13个):heap_no,代表的是当前这行数据在记录堆里的位置
- 第22-24位(3个):record_type,这就是说这行数据的类型
- 0代表的是普通类型
- 1代表的是B+树非叶子节点
- 2代表的是最小值数据
- 3代表的是最大值数据
- 第25-40位(16个):next_record,这指向下一条数据的指针
实际数据磁盘存储
在数据每一行实际存储的时候,会在真实的数据部分加入一些隐藏字段
- DB_ROW_ID字段:一个行的唯一标识,是他数据库内部给你搞的一个标识,不是主键ID字段。如果没有指定主键和unique key唯一索引的时候,就会在内部自动加一个ROW_ID作为主键
- DB_TRX_ID字段:跟事务相关的,他是说这是哪个事务更新的数据,这是事务ID
- DB_ROLL_PTR字段:回滚指针,是用来进行事务回滚
加入隐藏字段后,那么可能实际存储的数据各位为:
0x14 0x05 0x0a 00000101 0000000000000000000010000000000000011001 00000000094C(DB_ROW_ID)
00000000032D(DB_TRX_ID) EA000010078E(DB_ROL_PTR) hello java mysql ni hao
行数据溢出
数据页默认大小是16KB,如果一行数据大于16KB,例如varchar(65532)那么远远的超过16KB,那么这一行的数据就已经超过一个数据页的大小,肯定一个数据页是无法存储下的。在这种情况下,实际会在那一页存储这行数据,然后在那个字段中,仅仅包含一部分数据,同时包含一个20个字节的指针,指向了其它的数据页,那些数据页用链表串联起来,存放这个varchar(65532)超大字段的数据。
上面的过程就就叫做行溢出, 一行数据存储的内容太多了,一个数据页存储不下,这行数据溢出这个数据页,把数据溢出存放到其他数据页去,那么数据页就叫做溢出页。比如TEXT,BLOB,JSON这种类型的字段,都有可能发生行溢出情况,那么就会存储到多个数据页中,这行数据在Buffer Pool中就可能会存在多个缓存页里,刷入磁盘的时候,也会用多个数据页来存储这行数据。