1、一行数据在磁盘上是如何存储的?

那么接着我们可以来思考一下,对数据页中的每一行数据,他在磁盘上是怎么存储的?

其实这里涉及到一个概念,就是行格式。我们可以对一个表指定他的行存储的格式是什么样的,比如我们这里用一个 COMPACT格式。

CREATE TABLE table_name (columns) ROW_FORMAT=COMPACT ALTER TABLE table_name ROW_FORMAT=COMPACT

你可以在建表的时候,就指定一个行存储的格式,也可以后续修改行存储的格式。这里指定了一个COMPACT行存储 格式,在这种格式下,每一行数据他实际存储的时候,大概格式类似下面这样:

变长字段的长度列表,null值列表,数据头,column01的值,column02的值,column0n的值……

对于每一行数据,他其实存储的时候都会有一些头字段对这行数据进行一定的描述,然后再放上他这一行数据每一列 的具体的值,这就是所谓的行格式。除了COMPACT以外,还有其他几种行存储格式,基本都大同小异

2、对于VARCHAR这种变长字段,在磁盘上到底是如何存储的?

大家都知道,在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 大家可以看到,两行数据在底层磁盘文件里是不是挨着存储的? 没错!其实平时你看到的表里的很多行数据,最终落地到磁盘里的时候,都是上面那种样子的,一大坨数据放在一个 磁盘文件里都挨着存储的。

1、存储在磁盘文件里的变长字段,为什么难以读取?

现在我们来继续思考一个问题,假设现在我们要读取上面的磁盘文件里的数据,要读取出来hello a a这一行数据。那 你觉得是那么容易的吗? 当然不是了!这个过程比你想象的可能要困难一些。 假如现在你要读取hello a a这行数据,第一个问题就是,从这个磁盘文件里读取的时候,到底哪些内容是一行数据? 我不知道啊! 因为这个表里的第一个字段是VARCHAR(10)类型的,第一个字段的长度是多少我们是不知道的! 所以有可能你读取出来“hello a a hi”是一行数据,也可能是你读取出来“hello a”是一行数据,你在不知道一行数 据的每个字段到底是多少长度的情况下,胡乱的去读取是不现实的,根本不知道磁盘文件里混成一坨的数据里,哪些 数据是你要读取的一行?

2、引入变长字段的长度列表,解决一行数据的读取问题

所以说才要在存储每一行数据的时候,都保存一下他的变长字段的长度列表,这样才能解决一行数据的读取问题。 也就是说,你在存储“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

3、引入变长字段长度列表后,如何解决变长字段的读取问题?

所以假设此时你要读取“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”这行数据。

4、如果有多个变长字段,如何存放他们的长度?

接着我们假设,如果说有多个变长字段,如何存放他们的长度? 比如一行数据有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

3、一行数据中的多个NULL字段值在磁盘上怎么存储?

1、为什么一行数据里的NULL值不能直接存储?

之前我们已经给大家讲了在数据库里一行数据中如果有VARCHAR(10)之类的变长字段,那么他的存储和读取会有什么 问题,以及为了解决这个问题,为什么要给磁盘上存储的每一行数据都加入变长字段长度列表。 今天我们继续给大家讲解在磁盘上存储的一行数据里另外一块特殊的数据区域,就是NULL值列表。 这个所谓的NULL值列表,顾名思义,说的就是你一行数据里可能有的字段值是NULL,比如你有一个name字段,他是 允许为NULL的,那么实际上在存储的时候,如果你没给他赋值,他这个字段的值就是NULL。 好,那么假设这个字段的NULL值我们在磁盘上存储的时候,就是按照“NULL”这么个字符串来存储,是不是很浪费 存储空间? 本来他就是个NULL,说明什么值都没有,你还给他存个“NULL”字符串,你说你这是干什么呢? 所以实际在磁盘上存储数据的时候,一行数据里的NULL值是肯定不会直接按照字符串的方式存放在磁盘上浪费空间 的。

2、NULL值是以二进制bit位来存储的

我们接着看,那么NULL值列表在磁盘上到底应该如何存储呢? 很简单,对所有的NULL值,不通过字符串在磁盘上存储,而是通过二进制的bit位来存储,一行数据里假设有多个字段 的值都是NULL,那么这多个字段的NULL,就会以bit位的形式存放在NULL值列表中。 现在我们来给大家举个例子,假设你有一个表的额,他的建表语句如下所示:

  1. CREATE TABLE customer (
  2. name VARCHAR(10) NOT NULL,
  3. address VARCHAR(20),
  4. gender CHAR(1),
  5. job VARCHAR(30),
  6. school VARCHAR(50)
  7. ) ROW_FORMAT=COMPACT;

上面那个表就是一个假想出来的客户表,里面有5个字段,分别为name、address、genderjob、school,
就代表了 客户的姓名、地址、性别、工作以及学习小。 其中有4个变长字段,还有一个定长字段,然后第一个name字段是声明了NOT NULL的,就是不能为NULL,其他4个 字段都可能是NULL的。 那么现在我们来假设这个表里有如下一行数据,现在来看看,他在磁盘上是怎么来存储的:“jack NULL m NULL xx_school”,他的5个字段里有两个字段都是NULL

3、结合小小案例来思考一行数据的磁盘存储格式

接着我们来思考上面那个表里的那行案例数据,在磁盘上应该如何存储呢,因为他有多个变长字段,还有多个字段允 许为NULL。首先我们先回顾一下,一行数据在磁盘上的存储格式应该是下面这样的: 变长字段长度列表 NULL值列表 头信息 column1=value1 column2=value2 … columnN=valueN 所以先看变长字段长度列表应该放什么东西,他一共有4个变长字段,那么按照我们上次说的,是不是应该按照逆序的 顺序,先放school字段的长度,再放job、address、name几个字段的值长度? 说起来是这样,但是其实这里要区分一个问题,那就是如果这个变长字段的值是NULL,就不用在变长字段长度列表里 存放他的值长度了,所以在上面那行数据中,只有name和school两个变长字段是有值的,把他们的长度按照逆序放在 变长字段长度列表中就可以了,如下所示: 0x09 0x04 NULL值列表 头信息 column1=value1 column2=value2 … columnN=valueN 接着来看NULL值列表,这个NULL值列表是这样存放的,你所有允许值为NULL的字段,注意,是允许值为NULL,不 是说一定值就是NULL了,只要是允许你为NULL的字段,在这里每个字段都有一个二进制bit位的值,如果bit值是1说 明是NULL,如果bit值是0说明不是NULL。 比如上面4个字段都允许为NULL,每个人都会有一个bit位,这一行数据的值是“jack NULL m NULL xx_school”, 然后其中2个字段是null,2个字段不是null,所以4个bit位应该是:1010 但是实际放在NULL值列表的时候,他是按逆序放的,所以在NULL值列表里,放的是:0101,整体这一行数据看着是 下面这样的 0x09 0x04 0101 头信息 column1=value1 column2=value2 … columnN=valueN 另外就是他实际NULL值列表存放的时候,不会说仅仅是4个bit位,他一般起码是8个bit位的倍数,如果不足8个bit位 就高位补0,所以实际存放看起来是如下的: 0x09 0x04 00000101 头信息 column1=value1 column2=value2 … columnN=valueN

4、磁盘上的一行数据到底如何读取出来的?

我们结合上面的磁盘上的数据存储格式来思考一下,一行数据到底是如何读取出来的呢? 再看上面的磁盘数据存储格式: 0x09 0x04 00000101 头信息 column1=value1 column2=value2 … columnN=valueN 首先他必然要把变长字段长度列表和NULL值列表读取出来,通过综合分析一下,就知道有几个变长字段,哪几个变长 字段是NULL,因为NULL值列表里谁是NULL谁不是NULL都一清二楚。 此时就可以从变长字段长度列表中解析出来不为NULL的变长字段的值长度,然后也知道哪几个字段是NULL的,此时 根据这些信息,就可以从实际的列值存储区域里,把你每个字段的值读取出来了。 如果是变长字段的值,就按照他的值长度来读取,如果是NULL,就知道他是个NULL,没有值存储,如果是定长字 段,就按照定长长度来读取,这样就可以完美的把你一行数据的值都读取出来了!

4、磁盘文件中, 40个bit位的数据头以及真实数据是如何存储的?

之前我们已经给大家讲到了在磁盘上存储数据的时候,每一行数据都会有变长字段长度列表,逆序存放这行数据里的 变长字段的长度,然后会有NULL值列表,对于允许为NULL的字段都会有一个bit位标识那个字段是否为NULL,也是 逆序排列的。 今天我们接着给大家讲每一行数据存储的时候,还得有40个bit位的数据头,这个数据头是用来描述这行数据的。 这40个bit位里,第一个bit位和第二个bit位,都是预留位,是没任何含义的。 然后接下来有一个bit位是delete_mask,他标识的是这行数据是否被删除了,其实看到这个bit位,很多人可能已经反 映过来了,这么说在MySQL里删除一行数据的时候,未必是立马把他从磁盘上清理掉,而是给他在数据头里搞1个bit 标记他已经被删了? 没错,其实大家现在看这些数据头,只要先留有一个印象就可以了,知道每一行数据都有一些数据头,不同的数据头 都是用来描述这行数据的一些状态和附加信息的。 然后下一个bit位是min_rec_mask,这个bit位大家现在先不用去关注,他的含义以后我们讲到对应的内容的时候再 说,他其实就是说在B+树里每一层的非叶子节点里的最小值都有这个标记。 接下来有4个bit位是n_owned,这个暂时我们也先不用去管他,他其实就是记录了一个记录数,这个记录数的作用, 后续我们讲到对应的概念时会告诉大家的。 接着有13个bit位是heap_no,他代表的是当前这行数据在记录堆里的位置,现在大家可能也很难去理解他,这些概念 都要结合后续的一些内容才能理解的,这里只能是初步的给大家介绍下。 然后是3个bit位的record_type,这就是说这行数据的类型 0代表的是普通类型,1代表的是B+树非叶子节点,2代表的是最小值数据,3代表的是最大值数据 很多朋友可能也不理解这些什么意思,其实我们也现在不用在乎他,因为很多这些概念都是往后在讲解索引之类的技 术的时候才会涉及到的。 最后是16个bit的next_record,这个是指向他下一条数据的指针。

5、我们每一行的实际数据在磁盘上是如何存储的?

之前我们已经给大家讲过了,一行数据在磁盘文件里存储的时候,实际上首先会包含自己的变长字段的长度列表,然后是 NULL值列表,接着是数据头,然后接着才是真实数据,所以这一次我们就讲讲真实数据是如何存储的。 首先我们在存储真实数据的时候,并没什么特别的,无非就是按照我们那个字段里的数据值去存储就行了 比如我们之前说了一个例子,有一行数据是“jack NULL m NULL xx_school”,那么他真实存储大致如下所示: 0x09 0x04 00000101 0000000000000000000010000000000000011001 jack m xx_school 刚开始先是他的变长字段的长度,用十六进制来存储,然后是NULL值列表,指出了谁是NULL,接着是40个bit位的数据头, 然后是真实的数据值,就放在后面。 在读取这个数据的时候,他会根据变长字段的长度,先读取出来jack这个值,因为他的长度是4,就读取4个长度的数据,jack 就出来了; 然后发现第二个字段是NULL,就不用读取了; 第三个字段是定长字段,直接读取1个字符就可以了,就是m这个值; 第四个字段是NULL,不用读取了; 第五个字段是变长字段长度是9,读取出来xx_school就可以了。 但是等等,大家觉得真正在磁盘上存储的时候,我们那些字符串就是直接这么存储在磁盘上吗? 显然不是的! 实际上字符串这些东西都是根据我们数据库指定的字符集编码,进行编码之后再存储的,所以大致看起来一行数据是如下所示 的: 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 我给上面几个隐藏字段都加了括号说明了,上面那基本就是最终在磁盘上一行数据是长成什么样的了 我们再看看下面的图,大家回忆一下之前我们给大家讲解的,当你执行crud的时候,先会把磁盘上的数据加载到Buffer Pool 里缓存,然后更新的时候也是更新Buffer Pool的缓存,同时维护一堆链表。 然后定时或者不定时的,根据flush链表和lru链表,Buffer Pool里的更新过的脏数据就会刷新到磁盘上去。
好,现在我们再结合最近讲解的一些内容思考一下,那么在磁盘上的数据,每一行数据是不是就是类似“0x09 0x04 00000101 0000000000000000000010000000000000011001 00000000094C(DB_ROW_ID)00000000032D(DB_TRX_ID) EA000010078E(DB_ROL_PTR) 616161 636320 6262626262”这样的东西? 所以现在我们就初步的把磁盘上的数据和内存里的数据给关联起来了,他每一行数据的真实存储结构我们就了解了,希望大家 能在头脑里屡清楚这个他们之间的关系,其实这些都是有机一体的。

6、 理解数据在磁盘上的物理存储之后,聊聊行溢出是什么东西?

上一篇文章我们已经理解清楚了一行数据在磁盘上的物理存储结构了,其实理解了这个,你也就理解了每一行数据在磁盘上是 如何存储的,以及他被加载到缓存里来的时候,一行数据都包含哪些东西了。 今天我们来聊聊行数据的物理存储的一个高阶的话题,就是行溢出到底是个什么东西? 我们之前已经初步了解到,实际上我们每一行数据都是放在一个数据页里的,这个数据页默认的大小是16KB,那么之前就有 人在后台提过一个问题:万一 一行数据的大小超过了页的大小怎么办呢? 比如有一个表的字段类型是VARCHAR(65532),意思就是最大可以包含65532个字符,那也就是65532个字节,这就远大于 16kb的大小了,也就是说这一行数据的这个字段都远超一个数据页的大小了! 这个时候实际上会在那一页里存储你这行数据,然后在那个字段中,仅仅包含他一部分数据,同时包含一个20个字节的指针, 指向了其他的一些数据页,那些数据页用链表串联起来,存放这个VARCHAR(65532)超大字段里的数据。
上面说的这个过程,其实就叫做行溢出,就是说一行数据存储的内容太多了,一个数据页都放不下了,此时只能溢出这个数据 页,把数据溢出存放到其他数据页里去,那些数据页就叫做溢出页。 包括其他的一些字段类型都是一样的,比如TEXT、BLOB这种类型的字段,都有可能出现溢出,然后一行数据就会存储在多个 数据页里。 讲到这里,其实就已经把我们的行数据的物理存储相关的内容都已经讲完了,很多琐碎和细节的东西,其实不需要我们在这里 来死扣他,大家其实要理解的,就是一行数据的物理存储结构,然后这个数据其实是在一个数据页里的,如果一个数据页里放 不下一行数据,就会有行溢出问题,存放到多个数据页里去。 讲到这里,我们可以做一点总结,当我们在数据库里插入一行数据的时候,实际上是在内存里插入一个有复杂存储结构的一行 数据,然后随着一些条件的发生,这行数据会被刷到磁盘文件里去。 在磁盘文件里存储的时候,这行数据也是按照复杂的存储结构去存放的。 而且每一行数据都是放在数据页里的,如果一行数据太大了,就会产生行溢出问题,导致一行数据溢出到多个数据页里去,那 么这行数据在Buffer Pool可能就是存在于多个缓存页里的,刷入到磁盘的时候,也是用磁盘上的多个数据页来存放这行数据 的。 希望大家能够把最近几天学到的行数据物理存储结构,与之前学到的Buffer Pool缓存机制结合起来去理解,把他们有机的融合 为一体。 接下来,我们就会开始讲解数据页的物理存储结构,然后是表空间的物理存储结构,最后是讲解这些数据以物理存储结构的方 式,在磁盘上存储的时候,是放在哪些磁盘文件里的。 只要把后续那些内容讲完,那么大家就对数据库的Buffer Pool缓冲读写机制,以及磁盘上的物理存储机制,就完全理解了,而 且这两个机制都是有机结合在一起的,Buffer Pool的数据是从磁盘上读取出来的,Buffer Pool里更新的数据又会刷新到磁盘 上去。 在这个过程中,整个数据的物理存储机制,包括行数据、数据页、表空间、磁盘文件,这些概念,大家也都会理解了,到时候 自然理解了数据在磁盘上如何存储的,加载到Buffer Pool缓存页之后如何存储的。

7、 表空间以及划分多个数据页的数据区,又是什么概念?

上一次我们讲完了数据页的具体存储结构,当然里面有很多的细节我们还没讲,实际上现在也确实没必要去说那些细 节,因为很多数据页的一些细节性的东西,都是要在后续讲解的内容中涉及到的,比如说数据的删除,查询的一些原 理。 现在我们在大致了解了数据页的结构和使用之后,我们可以继续来了解下一个概念,就是表空间和数据区的概念 首先我们先说一下,什么是表空间? 简单来说,就是我们平时创建的那些表,其实都是有一个表空间的概念,在磁盘上都会对应着“表名.ibd”这样的一个 磁盘数据文件 所以其实在物理层面,表空间就是对应一些磁盘上的数据文件。 有的表空间,比如系统表空间可能对应的是多个磁盘文件,有的我们自己创建的表对应的表空间可能就是对应了一个 “表名.ibd”数据文件。 然后在表空间的磁盘文件里,其实会有很多很多的数据页,因为大家都知道一个数据页不过就是16kb而已,总不可能 一个数据页就是一个磁盘文件吧。 所以一个表空间的磁盘文件里,其实是有很多的数据页的。 但是现在有一个问题,就是一个表空间里包含的数据页实在是太多了,不便于管理,所以在表空间里又引入了一个数 据区的概念,英文就是extent 一个数据区对应着连续的64个数据页,每个数据页是16kb,所以一个数据区是1mb,然后256个数据区被划分为了一 组。 对于表空间而言,他的第一组数据区的第一个数据区的前3个数据页,都是固定的,里面存放了一些描述性的数据。比 如FSP_HDR这个数据页,他里面就存放了表空间和这一组数据区的一些属性。 IBUF_BITMAP数据页,里面存放的是这一组数据页的所有insert buffer的一些信息。 INODE数据页,这里也是存放了一些特殊的信息 大家暂时先不用了解这些东西具体是干什么的,你只要知道每一个组数据区的第一个数据区的前3个数据页,都是存放 一些特殊的信息的。 然后这个表空间里的其他各组数据区,每一组数据区的第一个数据区的头两个数据页,都是存放特殊信息的,比如 XDES数据页就是用来存放这一组数据区的一些相关属性的,其实就是很多描述这组数据区的东西,现在大家也不用去 知道是什么。 其实今天的内容讲到这里就差不多了,讲太多大家可能就被绕晕了,大家只要知道,我们平时创建的那些表都是有对 应的表空间的,每个表空间就是对应了磁盘上的数据文件,在表空间里有很多组数据区,一组数据区是256个数据区, 每个数据区包含了64个数据页,是1mb 然后表空间的第一组数据区的第一个数据区的头三个数据页,都是存放特殊信息的; 表空间的其他组数据区的第一个数据区的头两个数据页,也都是存放特殊信息的。大家今天只要了解到这个程度就可 以了。 所以磁盘上的各个表空间的数据文件里是通过数据区的概念,划分了很多很多的数据页的,因此当我们需要执行crud 操作的时候,说白了,就是从磁盘上的表空间的数据文件里,去加载一些数据页出来到Buffer Pool的缓存页里去使 用。 我下面给出了一张图,图里就给出了一个表空间内部的存储结构,包括一组一组的数据区,每一组数据区是256个数据 区,然后一个数据区是64个数据页。