mysql指引(七):innodb页中数据行结构

mysql指引(六):InnoDB的基本结构 中,我们研究了innodb的基本结构,并简单了解了页内数据的组织结构和页之间的组织结构。本小节,我们就来看看页中各数据行的格式是怎么样的。

行格式概念和意义

行格式,即 row format,它的概念如下:

The row format of a table determines how its rows are physically stored, which in turn can affect the performance of queries and DML operations.

行格式规定了数据表中的每行数据在物理上是如何存储的,即规定了每行数据的物理组织结构。不同的行格式,是可以影响数据操作性能的。

具体来说,行格式的意义如下:

As more rows fit into a single disk page, queries and index lookups can work faster, less cache memory is required in the buffer pool, and less I/O is required to write out updated values.

上篇文章提到了,以逻辑页为单位去执行I/O操作,每页中能够存储多个数据行。

假如每个页中能够存储的数据行越多,则页的数量也会降低。所以相应的索引查找会更快,buffer pool 占用也会变少。

修改同样多的数据,如果页数量变少,则每个页中需要修改的数据也会增多。比如只有一个数据页,那么修改数据全部落在这个数据页中,则只需要一次写I/O即可完成。若有两个数据页,修改数据恰好落在两个数据页中,则需要两次I/O才行。

其实,简答的理解,只要把行格式类比为压缩算法即可。比如图片,就有jpeg、png等不同的格式,对应不同的压缩算法,大小也不一样。

理解行格式的关键信息

理解行格式的组成结构,最终的目的是为了后面其他的原理做铺垫。关于行格式网络上已经有很多相关的文章了,本节我们就以一种不同的梳理流程来看看行格式的关键因素。

我们将行格式看成是两部分组成:标识域 + 数据域。比如 1AC85424457F9D 这串十六进制数就是页中的一行数据,1AC8 是标识,后面的都是这行的数据内容。可能1A 表示了每列的字节数,可能 C8 表示了下一行数据在哪里等等。

这是非常简单的理解,下面就进一步看看。

数据域格式

数据域除了各列的数值,同样在前级自动插入了三个数据:

  • row ID:当后面的列中没有主键时,会自动插入这个row ID,用以区分每列数据
  • transaction ID:事务ID,这个和后面的知识点有关,此处忽略。
  • roll pointer:回滚指针,这个也和后面的知识点有关,此处忽略。

后面的列1列2等自然就是实际数据了,只不过和我们在数据表中看到的数据有些不同,这里的数据是二进制形式存储的。


这里我们回头看一个事情:

早在 数据表设计与mysql入门(二) 中,提到了比如你在设计数据表的时候,给一列数据的类型制定了 int(8),那么是不是这列就占用8个字节呢?答案是否定的。

int 类型是固定长度,所以只占用 4 个字节。比如列1是 int 类型,值为9,则映射到图中的数据域列1为:00 00 00 09(十六进制表示)。

假如你指定图中列1的数据为 char(4),这里的4表示固定存储4个字符,那么这列是定长的吗?

答案同样是否定的。比如这列字符集为UTF-8,存储数值为 aaaa,那长度就是4字节,因为UTF-8编码下,英文字母用一个字节表示,所以如果这列的数据都是英文字母,则在一定意义上是定长列,就是4字节。

但是,当这列存储的是 “你好aa”,则在UTF-8编码下,总共占用 3+3+1+1 = 8 个字节,一个中文占用3字节。所以这样来看,这里长度又是变化的。

假如列1此时存储 aaaa,然后后续被更新为 “你好aa”,则由于列1占用的物理空间变长了,也会带来一系列影响。比如这行数据可能会换到其他页面等等。这里的思考保留。

下面重点来看下标识域的格式。

标识域格式

NULL标记

首先,我们问一个问题,即为NULL值的列如何存储。

前文说了,行格式的目的之一就是提升性能,即在一个页中尽量多的存储数据行。自然地,我们不希望NULL值占用存储空间,也就是不要在数据域中存储NULL值。

那么我们可以在标识域中标记哪些列为NULL(这个标记实际也是占用存储空间的,但是比单纯的记录NULL值要少得多),比如:

十六进制 03 = 二进制的 0000 0101 ,这样子右边为最低位,对应列1。1表示是NULL,0表示不是NULL

则,03 就表示 列1 和列3 是NULL值,其他列不是NULL。

到这里,标识域格式为:

变长列长度标记

针对上一小节提到的变长列问题,我们自然要问,变长列如何处理?因为数据域只存储数据,那么肯定要知道每列存储了几个字节,这样才能拆分数据。所以,对于变长列来说,其长度也需要在标识域中标识出来。

但是,不禁要问,那么定长列的长度在哪里标识呢?这个我们后面会提到,暂时忽略。

到这里,标识域格式为:

06 05 01 分别表示三个变长列的实际长度(至于是06 表示列1呢,还是01 表示列1呢?这里先留下这个思考)

变长列这块还有几个问题需要探讨,暂时先放下,我们来看看标识域还有什么东西

记录头信息

因为实际数据是以二进制形式存储的,我们根据变长列表和定长长度可以将二进制数据切分成每一列数据,针对数据中特殊的NULL值类型,可以通过NULL标记来区分。这样子,针对数据本身存取的处理就完成了。那么,行格式还需要标记些什么呢?

  • 由上文我们看到的,行与行之间是通过类似链表的结构连接在一起的。所以记录头就需要包含下一行数据在哪里?
  • 由上文我们看到的,索引与叶子节点的实际数据都是用同一种页结构表示的,所以数据行结构同时可以当做是索引行的结构。记录头需要包含该数据行到底是什么类型
  • 当我们删除一行数据的时候,实际上并不是将数据域内容清空,也没必要。只需要标记这行数据被删除了即可。所以该行数据是否删除的标志也需要在记录头中

记录头总共5字节,40位。这40位就包含了上述三种信息以及其他的关键信息。

到这里,记录头中的格式如下:

因为通过变长列表和定长长度我们就可以知道数据域的长度,同样地就知道了下一条数据应该存在哪里,偏移量就是从记录头开始往后数多少个字节就是下一行数据的数据域起始地址。偏移量占2个字节。

数据类型则有四种,占3个bit:

  • 000:表示该行记录就是普通记录,即存放实际记录的数据行
  • 001:表示该行记录是非叶子节点的数据,即存放的是索引,对于主键索引来说,就是主键ID和实际记录的页号
  • 010:表示Infimum,后面会继续说明。
  • 011:表示Supremum,后面会继续说明。

是否被删除,占1个bit,1表示被删除了。

除了这三块外,记录头中还有其他信息,我们现归类为 其他信息域,后续会继续说明。此时,格式如下,偏移量调整到最后面:

行之间的连接

看了一行中的信息格式后,我们来看下行之间的连接。上面说了,使用偏移量来指向下一行数据,实际结构如图:

变长列存储问题

比如存储 TEXT 类型数据,很可能这一列非常长,页中放不下,那应该怎么办呢?

此时引入一种新类型的页,叫做溢出页,专门存储这种在一个数据页中放不下的数据。这列也被称作为溢出列。

由于标识域中的变长列长度标识已经说明了这列的总长度,而数据域无法存下,那么就有两种方案来处理:

compact行格式方案

在compact行格式方案下,溢出列的数据一部分存在数据页的数据域中,一部分存储在溢出页中,所以结构为:

dynamic行格式方案

在Dynamic行格式方案下,溢出列的数据全部放到溢出页中,结构为:

行格式小结

实际上,行格式有多种,常见的就是 Compact行格式和 dynamic行格式。二者的区别只在于变长列的存储区别,本文描述的格式结构就是这两种行格式的结构。

记录头中还有一些关键字段没有提及,后文页结构会再返回来看的。

下一篇文章,我们就来看看 innodb 的 页结构

发布了47 篇原创文章 · 获赞 91 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/zhou307/article/details/104383861