一行数据在磁盘上是如何存储的
对数据页中的每一行数据,他在磁盘上是怎么存储的?其实这里涉及到一个概念,就是行格式。我们可以对一个表指定他的行存储的格式是什么样的,比如我们这里用一个COMPACT格式。
你可以在建表的时候,就指定一个行存储的格式,也可以后续修改行存储的格式。
这里指定了一个COMPACT行存储格式,在这种格式下,每一行数据他实际存储的时候,大概格式类似下面这样:变长字段的长度列表,null值列表,数据头,column01的值,column02的值,column0n的值……
对于每一行数据,他其实存储的时候都会有一些头字段对这行数据进行一定的描述,然后再放上他这一行数据每一列的具体的值,这就是所谓的行格式。除了COMPACT以外,还有其他几种行存储格式,基本都大同小异。
变长字段在磁盘中是怎么存储的?
大家都知道,在MySQL里有一些字段的长度是变长的,是不固定的,比如VARCHAR(10)之类的这种类型的字段,实际上他里面存放的字符串的长度是不固定的,有可能是“hello”这么一个字符串,也可能是“a”这么一个字符串。
现在有一行数据,他的几个字段的类型为VRACHAR(10),CHAR(1),CHAR(1),那么他第一个字段是VARCHAR(10),这个长度是可能变化的,所以这一行数据可能就是类似于:hello a a,这样子,第一个字段的值是“hello”,后面两个字段的值都是一个字符,就是一个a。然后另外一行数据,同样也是这几个字段,他的第一个字段的值可能是“hi”,后面两个字段也是“a”,所以这一行数据可能是类似于:hi a a。一共三个字段,第一个字段的长度是是不固定的,后面两个字段的长度都是固定的1个字符。
那么现在,我们来假设你把上述两条数据写入了一个磁盘文件里,两行数据是挨在一起的,那么这个时候在一个磁盘文件里可能有下面的两行数据:hello a a hi a a
没错!其实平时你看到的表里的很多行数据,最终落地到磁盘里的时候,都是上面那种样子的,一大坨数据放在一个磁盘文件里都挨着存储的。
存储在磁盘文件里的变长字段,为什么难以读取?
假设现在我们要读取上面的磁盘文件里的数据,要读取出来hello a a这一行数据。这个过程比你想象的可能要困难一些。
假如现在你要读取hello a a这行数据,第一个问题就是,从这个磁盘文件里读取的时候,到底哪些内容是一行数据?我不知道啊!因为这个表里的第一个字段是VARCHAR(10)类型的,第一个字段的长度是多少我们是不知道的!所以有可能你读取出来“hello a a hi”是一行数据,也可能是你读取出来“hello a”是一行数据,你在不知道一行数据的每个字段到底是多少长度的情况下,胡乱的去读取是不现实的,根本不知道磁盘文件里混成一坨的数据里,哪些数据是你要读取的一行?
引入变长字段的长度列表,解决一行数据的读取问题
所以说才要在存储每一行数据的时候,都保存一下他的变长字段的长度列表,这样才能解决一行数据的读取问题。也就是说,你在存储“hello a a”这行数据的时候,要带上一些额外的附加信息,比如第一块就是他里面的变长字段的长度列表。也就是说,这个hello是VARCHAR(10)类型的变长字段的值,那么这个“hello”字段值的长度到底是多少?我们看到“hello”的长度是5,十六进制就是0x05,所以此时会在“hello a a”前面补充一些额外信息,首先就是变长字段的长度列表,你会看到这行数据在磁盘文件里存储的时候,其实是类似如下的格式:0x05 null值列表 数据头 hello a a。这个时候假设你有两行数据,还有一行数据可能就是:0x02 null值列表 数据头 hi a a,两行数据放在一起存储在磁盘文件里,看起来是如下所示的:
0x05 null值列表 数据头 hello a a 0x02 null值列表 数据头 hi a a
引入变长字段长度列表后,如何解决变长字段的读取问题?
所以假设此时你要读取“hello a a”这行数据,你首先会知道这个表里的三个字段的类型是VARCHAR(10) CHAR(1) CHAR(1),那么此时你先要读取第一个字段的值,那么第一个字段是变长的,到底他的实际长度是多少呢?
此时你会发现第一行数据的开头有一个变长字段的长度列表,里面会读取到一个0x05这个十六进制的数字,发现第一个变长字段的长度是5,于是按照长度为5,读取出来第一个字段的值,就是“hello”。接着你知道后续两个字段都是CHAR(1),长度都是固定的1个字符,于是此时就依次按照长度为1读取出来后续两个字段的值,分别是“a”“a”,于是最终你会读取出来“hello a a”这一行数据!
接着假设你要读取第二行数据,你先看一下第二行数据后的变长字段长度列表,发现他第一个变长字段的长度是0x02,于是就读取长度为2的字段值,就是“hi”,再读取两个长度固定为1的字符值,都是“a”,此时读取出来“hi a a”这行数据。
如果有多个变长字段,如何存放他们的长度?
比如一行数据有VARCHAR(10) VARCHAR(5) VARCHAR(20) CHAR(1) CHAR(1),一共5个字段,其中三个是变长字段,此时假设一行数据是这样的:hello hi hao a a
此时在磁盘中存储的,必须在他开头的变长字段长度列表中存储几个变长字段的长度,一定要注意一点,他这里是逆序存储的!也就是说先存放VARCHAR(20)这个字段的长度,然后存放VARCHAR(5)这个字段的长度,最后存放VARCHAR(10)这个字段的长度。现在hello hi hao三个字段的长度分别是0x05 0x02 0x03,但是实际存放在变长字段长度列表的时候,是逆序放的,所以一行数据实际存储可能是下面这样的:0x03 0x02 0x05 null值列表 头字段 hello hi hao a a
为什么一行数据里的NULL值不能直接存储?
这个所谓的NULL值列表,顾名思义,说的就是你一行数据里可能有的字段值是NULL,比如你有一个name字段,他是允许为NULL的,那么实际上在存储的时候,如果你没给他赋值,他这个字段的值就是NULL。好,那么假设这个字段的NULL值我们在磁盘上存储的时候,就是按照“NULL”这么个字符串来存储,是不是很浪费存储空间?本来他就是个NULL,说明什么值都没有,你还给他存个“NULL”字符串,你说你这是干什么呢?所以实际在磁盘上存储数据的时候,一行数据里的NULL值是肯定不会直接按照字符串的方式存放在磁盘上浪费空间的。
NULL值是以二进制bit位来存储的
对所有的NULL值,不通过字符串在磁盘上存储,而是通过二进制的bit位来存储,一行数据里假设有多个字段的值都是NULL,那么这多个字段的NULL,就会以bit位的形式存放在NULL值列表中。
接着来看NULL值列表,这个NULL值列表是这样存放的,你所有允许值为NULL的字段,注意,是允许值为NULL,不是说一定值就是NULL了,只要是允许你为NULL的字段,在这里每个字段都有一个二进制bit位的值,如果bit值是1说明是NULL,如果bit值是0说明不是NULL。但是实际放在NULL值列表的时候,他是按逆序放的,所以在NULL值列表里,放的是:0101。
另外就是他实际NULL值列表存放的时候,不会说仅仅是4个bit位,他一般起码是8个bit位的倍数,如果不足8个bit位就高位补0,所以实际存放看起来是如下的:
0x09 0x04 00000101 头信息 column1=value1 column2=value2 … columnN=valueN
磁盘上的一行数据到底如何读取出来的?
首先他必然要把变长字段长度列表和NULL值列表读取出来,通过综合分析一下,就知道有几个变长字段,哪几个变长字段是NULL,因为NULL值列表里谁是NULL谁不是NULL都一清二楚。
此时就可以从变长字段长度列表中解析出来不为NULL的变长字段的值长度,然后也知道哪几个字段是NULL的,此时根据这些信息,就可以从实际的列值存储区域里,把你每个字段的值读取出来了。
如果是变长字段的值,就按照他的值长度来读取,如果是NULL,就知道他是个NULL,没有值存储,如果是定长字段,就按照定长长度来读取,这样就可以完美的把你一行数据的值都读取出来了!
磁盘文件中,40个bit位的数据头以及真实数据是如何存储的
每一行数据存储的时候,还得有40个bit位的数据头,这个数据头是用来描述这行数据的。
这40个bit位里,第一个bit位和第二个bit位,都是预留位,是没任何含义的。
然后接下来有一个bit位是delete_mask,他标识的是这行数据是否被删除了,其实看到这个bit位,很多人可能已经反映过来了,这么说在MySQL里删除一行数据的时候,未必是立马把他从磁盘上清理掉,而是给他在数据头里搞1个bit标记他已经被删了?
然后下一个bit位是min_rec_mask,他其实就是说在B+树里每一层的非叶子节点里的最小值都有这个标记。
接下来有4个bit位是n_owned,他其实就是记录了一个记录数。
接着有13个bit位是heap_no,他代表的是当前这行数据在记录堆里的位置。
然后是3个bit位的record_type,这就是说这行数据的类型,0代表的是普通类型,1代表的是B+树非叶子节点,2代表的是最小值数据,3代表的是最大值数据。
最后是16个bit的next_record,这个是指向他下一条数据的指针。
我们每一行的实际数据在磁盘上是如何存储的?
实际上字符串这些东西都是根据我们数据库指定的字符集编码,进行编码之后再存储的,所以大致看起来一行数据是如下所示的:
0x09 0x04 00000101 0000000000000000000010000000000000011001 616161 636320 6262626262
大家会看到上面,我们的字符串和其他类型的数值最终都会根据字符集编码,搞成一些数字和符号存储在磁盘上。在实际存储一行数据的时候,会在他的真实数据部分,加入一些隐藏字段。
首先有一个DB_ROW_ID字段,这就是一个行的唯一标识,是他数据库内部给你搞的一个标识,不是你的主键ID字段。如果我们没有指定主键和unique key唯一索引的时候,他就内部自动加一个ROW_ID作为主键。
接着是一个DB_TRX_ID字段,这是跟事务相关的,他是说这是哪个事务更新的数据,这是事务ID。
最后是DB_ROLL_PTR字段,这是回滚指针,是用来进行事务回滚的。
所以如果你加上这几个隐藏字段之后,实际一行数据可能看起来如下所示:
0x09 0x04 00000101 0000000000000000000010000000000000011001 00000000094C(DB_ROW_ID)00000000032D(DB_TRX_ID) EA000010078E(DB_ROL_PTR) 616161 636320 6262626262
行溢出是什么东西
我们之前已经初步了解到,实际上我们每一行数据都是放在一个数据页里的,这个数据页默认的大小是16KB,那么之前就有人在后台提过一个问题:万一 一行数据的大小超过了页的大小怎么办呢?
比如有一个表的字段类型是VARCHAR(65532),意思就是最大可以包含65532个字符,那也就是65532个字节,这就远大于16kb的大小了,也就是说这一行数据的这个字段都远超一个数据页的大小了!
这个时候实际上会在那一页里存储你这行数据,然后在那个字段中,仅仅包含他一部分数据,同时包含一个20个字节的指针,指向了其他的一些数据页,那些数据页用链表串联起来,存放这个VARCHAR(65532)超大字段里的数据。
上面说的这个过程,其实就叫做行溢出,就是说一行数据存储的内容太多了,一个数据页都放不下了,此时只能溢出这个数据页,把数据溢出存放到其他数据页里去,那些数据页就叫做溢出页。包括其他的一些字段类型都是一样的,比如TEXT、BLOB这种类型的字段,都有可能出现溢出,然后一行数据就会存储在多个数据页里。
表空间以及划分多个数据页的数据区,又是什么概念?
现在我们在大致了解了数据页的结构和使用之后,我们可以继续来了解下一个概念,就是表空间和数据区的概念。
简单来说,就是我们平时创建的那些表,其实都是有一个表空间的概念,在磁盘上都会对应着“表名.ibd”这样的一个磁盘数据文件。所以其实在物理层面,表空间就是对应一些磁盘上的数据文件。有的表空间,比如系统表空间可能对应的是多个磁盘文件,有的我们自己创建的表对应的表空间可能就是对应了一个“表名.ibd”数据文件。
然后在表空间的磁盘文件里,其实会有很多很多的数据页,因为大家都知道一个数据页不过就是16kb而已,总不可能一个数据页就是一个磁盘文件吧。所以一个表空间的磁盘文件里,其实是有很多的数据页的。但是现在有一个问题,就是一个表空间里包含的数据页实在是太多了,不便于管理,所以在表空间里又引入了一个数据区的概念,英文就是extent。
一个数据区对应着连续的64个数据页,每个数据页是16kb,所以一个数据区是1mb,然后256个数据区被划分为了一组。
对于表空间而言,他的第一组数据区的第一个数据区的前3个数据页,都是固定的,里面存放了一些描述性的数据。比如FSP_HDR这个数据页,他里面就存放了表空间和这一组数据区的一些属性。IBUF_BITMAP数据页,里面存放的是这一组数据页的所有insert buffer的一些信息。INODE数据页,这里也是存放了一些特殊的信息。
然后这个表空间里的其他各组数据区,每一组数据区的第一个数据区的头两个数据页,都是存放特殊信息的,比如XDES数据页就是用来存放这一组数据区的一些相关属性的,其实就是很多描述这组数据区的东西。
我们平时创建的那些表都是有对应的表空间的,每个表空间就是对应了磁盘上的数据文件,在表空间里有很多组数据区,一组数据区是256个数据区,每个数据区包含了64个数据页,是1mb。然后表空间的第一组数据区的第一个数据区的头三个数据页,都是存放特殊信息的;表空间的其他组数据区的第一个数据区的头两个数据页,也都是存放特殊信息的。
当我们需要执行crud操作的时候,说白了,就是从磁盘上的表空间的数据文件里,去加载一些数据页出来到Buffer Pool的缓存页里去使用。
