数据库给使用者最直观的感觉,就是库、表、行、字段,这些概念都是逻辑上的。前面我们深入讲解了Buffer Pool的内部原理,它的基本存储单位是默认大小为16K的页。每页都保存了一行一行的数据。我们按照数据页为单位把磁盘上的数据加载到内存的缓存页里来,也是按照页为单位,把缓存页的数据刷入磁盘的数据页中。
而我们常常听到数据页、数据区、表空间这些名词,其实这些名词是物理层面上的概念。我们不经要问,库、表、行、字段,这些逻辑上的概念是如何对应到物理层的概念上的呢?我们查询一行数据,是如何找到条数据所在的数据页的呢?
接下来,笔者用几篇文章,讲解下MySQL的表空间、数据区、数据页这些概念,当大家明白搞明白这些东西后,就自然理解上面的问题了。
行格式
行格式即行记录的物理存储格式,决定了这张表数据的物理存储方式,会影响crud性能。
指定行格式
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称;
ALTER TABLE 表名 ROW_FORMAT=行格式名称;
InnoDB 包含以下四种行格式
-
Compact
-
Redundant
-
Dynamic
-
Compressed
mysql5.7之前的版本使用的是compact行格式,5.7及以后的版本使用的是dynamic行格式。Compact和Dynamic应用较广泛,compact是目前使用最多的一种,而dynamic是新版本默认的行记录格式。
初识MySQL行数据存储格式
我们这里就以Compact存储格式来讲解。
Compact行格式下,每行数据的存储格式,大概是这样的:
变长字段(记录的长度)列表,null值列表,数据头,col1的值,col2的值,col3的值......
Compact 中一条完整的记录可以被分成'记录的额外信息'和'记录的真实数据' 两部分,其他三种存储存储格式基本大同小异。
变长字段是如何存储的?
MySQL中变长字段长度是不固定的, 比如VARCHAR(50),实际存储的内容可能是"hello",也可能是“hello world"。
所以,我们要读取这类数据字段,必须要知道它的长度。
比如一行数据,它的几个字段为VARCHAR(10), VARCHAR(5)
,VARCHAR(20),CHAR(1),CHAR(1),插入一行数据:hello, ni, hao, a, b。
此时磁盘中存储的行开头的变长字段长度列表,必须存储几个变长字段的长度,需要注意的是,这里是逆序存储的。
行数据存储格式是这样的:
0x03 0x02 0x05 null值列表 头字段 hello, ni, hao, a, b
NULL值列表
null值列表,说的就是一行数据里可能有的字段是选填的,值可以是null。比如name字段,如果允许为null,那么实际存储的时候,要标记出来。
为了节省存储空间,MySQL设计的时候,使用二进制的bit位来存储null值列表。
假如创建了一张表:
CREATE TABLE user (
name VARCHAR(10)NOT NULL,
address VARCHAR(20),
gender CHAR(1),
job VARCHAR(30),
school VARCHAR(50)
)ROW_FORMAT = COMPACT;
user表5个字段,4个变长的,只有name是非NULL的,其他的4个字段都是可以为NULL的。
假如现在插入一行数据:zhangsan NULL M NULL Tsinghua,address和job是NULL,那么它在磁盘上是怎么存储的?
NULL值列表,是这样存储的,你所有允许值为NULL的字段,都会有一个二进制的bit值,bit值为1说明是NULL,如果bit值为0说明不是NULL。
上面插入的那行数据,address、gender、job、school4个字段都允许为NULL,每个字段都会有一个bit位,其中address与job是NULL,所有4个bit位应该是:1010。
而且NULL值列表也是逆序存储的,所以NULL值列表里是0101。
一般NULL值列表是8个bit位的倍数,如果不足8个bit位,则高位补0。
所以现在看来,行数据存储格式是这样的:
0x08 0x08 00000101 头信息 col1的值,col2的值,col3的值......
数据头以及真实数据
行数据的头长度是固定的40bit,第一个bit和第二个bit,都是预留的,暂时没有用到。
记录头信息详细如下表所示,有的暂时没有用到,可以暂时不用深究它。
名称
大小 (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
表示下一条记录的相对位置
现在再加上数据头部分,上面的行存储格式就变成这样了,
0x08 0x08 00000101 0000000000000000000010000000000000011001 zhangsan M Tsinghua
刚开始先是变长字段的长度,用16进制表示,然后是NULL值列表,标识哪些值是NULL,接着是40bit的数据头,最后是真实数据。
在读取数据的时候,也会根据变长字段的长度,先读取出长度为8的zhangsan。
然后发现第二个字段是NULL,就不用再读了。
第三个字段是定长的1,就直接读取出gender为M。
第四个字段是NULL,就不用再读了。
第五个字段变长,长度是8,再读取长度为8的Tsinghua。
然而,真正磁盘上存储的时候,那些字符串就是直接存储在磁盘上的吗?
实际上,字符串都是根据数据库指定的字符集编码,进行编码之后再存储的,编码后大概是这样的:
0x08 0x08 00000101 0000000000000000000010000000000000011001 341324 134546 9342345
大家会看到上面,字符串和其他类型的数值最终都会根据数据库字符集编码,变成一些数字和符号存储在磁盘上。
最后,在实际存储一行数据的时候,MySQL还会给每条记录,加入一些隐藏字段。如下表:
列名
是否必须
占用空间 (bit)
描述
DB_ROW_ID
否
6
行ID,唯一标识一条记录
DB_TRX_ID
是
6
事务ID
DB_ROLL_PTR
是
7
回滚指针
如果用户没有指定主键,且表中没有Unique键时才会使用DB_ROW_ID作为主键。
最终,上面那条数据的存储格式,变成这样了:
0x08 0x08 00000101 0000000000000000000010000000000000011001 00000000034C(DB_ROW_ID)00000000036D(DB_TRX_ID) EA000010022B(DB_ROL_PTR) 341324 134546 9342345
到这里为止,我们基本把一行数据在磁盘上是如何存储的讲清楚了。
END
如果你喜欢本文,
请长按二维码,关注 南山的架构笔记