【MYSQL】MYSQL 的学习教程(二) 之 InnoDB 数据页结构

1、什么是数据页

1.1、 MYSQL 数据库的存储结构

在 MYSQL 中,数据记录、索引信息都是保存在文件上的,确切地说,是保存在结构中。

查看 MySQL 数据存储目录:

# 查看 MYSQL 数据存储目录
SHOW VARIABLES LIKE 'datadir';
SELECT @@datadir

执行之后,如图:
在这里插入图片描述

每创建一个数据库 database_name,这个目录下(包括自定义的)就会创建一个以数据库名为名的目录,然后里面存储表结构和数据文件。如图:
在这里插入图片描述

InnoDB 存储引擎创建的任何一张表的都会有两个文件:

  • .frm 文件:存储表结构 【MYSQL 8.0 版本取消 .frm 文件,并移动到 SDI,SDI 保存在 .ibd 文件】
  • .ibd/.ibdata 文件:存储数据。数据既可以存储在独享表空间文件中,也可以存储在共享表空间文件中。通过配置来决定是使用独享表空间还是用共享表空间存放存储数据
    • 独享表空间:表名.ibd 文件 【innodb_file_per_table = 1,MYSQL 5.6.6 版本之后默认】
    • 共享表空间:ibdata1 文件

1.2、引入数据页

为什么要引入数据页?

InnoDB 是一个将表中的数据持久化到磁盘上的存储引擎。即使,我们关闭并重启服务器,数据还是存在的。但是,当我们处理数据的时候需要把磁盘中的数据加载到内存中【在内存中处理数据】;如果是处理更新请求,还需要把内存中的数据刷新到磁盘上。
而我们知道读写磁盘的速度是非常慢的,与读写内存差了几个数量级。当我们想从表中获取某些记录时,InnoDB 存储引擎需要一条一条地把记录从磁盘上读出来么?这样会很慢。

所以,InnoDB 采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位

程序局部性原理:查询数据库时,不论读一行,还是读多行,都是将这些行所在的整页数据加载,然后在内存中匹配过滤出最终结果;表的检索速度跟树的深度有直接关系,毕竟一次页加载就是一次 IO,而磁盘 IO 又是比较费时间

1.3、数据页

数据页:将数据划分为若干个页,这些页在物理结构上可以不相连,只需要通过双向链表使其在逻辑结构上相连即可。每个数据页中的记录都会按照主键值从小到大的顺序组成一个单向链表。并且,每个数据页都会为存储在它里边的记录生成一个页目录,再通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录

数据页主要是用来存储表中记录的,它在磁盘中是用双向链表(支持 order by id asc 和 order by id desc )相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页(表中的数据比较多,在磁盘中可能存放在多个数据页)

要根据某个条件查询数据时,需要从一个数据页找到另一个数据页,这时候的双向链表就派上大用场了。磁盘中各数据页的整体结构如下图所示:
在这里插入图片描述

通常情况下,单个数据页默认的大小是 16kb。当然,我们也可以通过参数:innodb_page_size,来重新设置大小。不过,一般情况下,用它的默认值就够了

数据页是 InnoDB 存储引擎磁盘管理的最小单元,数据库每次读写都是以【页】为单位的,一次最少从磁盘中读取 16KB 的内容到内存中

1.4、InnoDB 存储引擎的逻辑存储结构

InnoDB 存储引擎的逻辑存储结构如图:
在这里插入图片描述

从 InnoDB 存储引擎的逻辑存储结构看:所有数据都被逻辑地存放在一个空间中,称之为表空间( tablespace)。表空间又由段(segment)、区( extent)、页(page)组成。页在一些文档中有时也称为块( block)。

表空间表示一本书,段表示书中的章节,区表示每章节的小节,页表示书的每一页,行就是每页的每行数据。表空间里有多个段,一个段包含 256 个区,一个区包含 64 个页,一个页为 16 KB

在 InnoDB 存储引擎中,数据是按照表空间来组织存储的。表空间是表空间文件是实际存在的物理文件。
当我们创建一个表之后,在磁盘上会有对应的表名称 .ibd 的磁盘文件,这个文件就是这张表的表空间物理文件

  1. 表空间:用于存储存储一个或多个ibd数据文件(记录和索引),每个表空间都具有一个唯一的表空间 id。
    ● Mysql 5.6 版本默认所有 InnoDB 的所有表数据会放在一个系统表空间 ibdata1
    ● 5.7 版本之后,每个表的数据默认单独放到一个独立表空间内。但每张表的独立表空间只存放数据页、索引页和写缓冲 BitMap 页,其他信息如回滚页、插入缓冲索引页、二次写缓冲仍放在系统表空间(共享表)。所以即使每个表的数据单独放到自己的独立表空间,系统表空间也会不断增大
  2. :表空间由多个段组成的,段是由多个区组成的。段一般可分为数据段、索引段和回滚段等
    • 数据段:存放 B+Tree 叶子节点区的集合
    • 索引段:存放 B+Tree 非叶子节点区的集合
    • 回滚段:存放回滚数据区的集合(MVCC 就是利用了回滚段实现了多版本查询控制)
  3. :一个区固定包含 64 个连续的页,大小为 1M。当表文件空间不足,不会一页页的分配,而是直接分配一个区大小的空闲空间给 ibd 文件【这样就使得相邻的页的物理位置也是相邻的,就可以使用顺序 I/O 了,效率高】。B+Tree 的每一层节点之间都是通过双向链表链接的,以页为单位,相邻的两个页之间位置并不是连续的(逻辑上连接),可能离得非常远,如果数据量大的情况下,查询会产生大量的随机 I/O(效率低)。为了解决这个问题,为某个索引分配空间的时候就按照区为单位
  4. :记录是按行存储的,但是数据库的读取并不是以行为单位的,而是以页为单位,每页的大小为 16KB。如果每次读取数据只能读取一行、处理一行数据,那么频繁的 I/O 操作会使效率非常低
    • 数据页:B+树的叶子节点页
    • 索引页:B+树非叶子节点页
  5. :数据库中的数据都是按行存储的,行记录根据不同的行格式,有不同的存储结构

2、数据页结构

数据页结构图如下:
在这里插入图片描述

2.1、文件头部【38 字节】

InnoDB 能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中
如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?
这时就需要使用文件头部了。它里面包含了多个信息。如下:
在这里插入图片描述

其中,我们需要重点关注一下标黄的属性:

  • FIL_PAGE_OFFSET(4 字节):页号,用来定位唯一的页
  • FIL_PAGE_PREV(4 字节):上一页页号
  • FIL_PAGE_NEXT(4字节):下一页页号

InnoDB 是通过页号、上一页页号和下一页页号来串联不同数据页的【这些页与页之间在逻辑上连接而非物理连接】。如下图所示:
在这里插入图片描述
不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。

  • FIL_PAGE_TYPE(2 字节):代表当前页的类型。InnoDB 为了不同的目的而把页分为不同的类型

在这里插入图片描述

  • FIL_PAGE_SPACE_OR_CHKSUM(4字节):当前页面的校验和(checksum)

⭐ 什么是校验和?
就类似于一个很长的字符串,通过某种算法计算出一个比较短的值来代表这个字符串,这个比较短的值就是校验和。
⭐ 为什么要使用校验和?
在比较两个很长的字符串时,如果直接进行比较的话,肯定会比较慢的。但是,如果通过比较两个字符串的校验和(其中,生成校验和耗时可以忽略不计):校验和相同就代表两个字符串相同,反之则不同。这种方式很明显会缩短比较时的耗时。
⭐ 校验和在页面上有什么作用?
校验和这个属性是存在于文件头部和文件尾部的。InnoDB 以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间就需要把数据同步刷新到磁盘中了。但是同步时可能会出现断电、磁盘损坏等一系列不可抗因素,从而造成数据页传输的不完整。
为了检测一个页是否刷盘成功,就可以通过文件头部的校验和与文件尾部的校验和做对比,如果两个值不相等则说明页刷盘失败,需要重新刷盘(数据重试或回滚操作);否则认为页刷盘成功。

  • FIL_PAGE_LSN(8 字节):页面被最后修改时对应的日志序列位置(Log Sequence Number,简称:LSN)

2.2、文件尾部【8 字节】

  • 前 4 个字节:代表页的校验和,这个部分与 File Header 中的校验和相对应。
  • 后 4 个字节:代表页面被最后修改时对应的日志序列位置(LSN),这个部分也是为了校验页的完整性,如果首部和尾部的 LSN 值校验不成功的话,也说明同步过程出现了错了(刷盘失败)

2.3、空闲空间【不确定】

当我们存储一条数据的时候,存储的记录会按照指定的行格式存储到 User Records 部分。但是在最开始生成页的时候,其实是没有 User Record 这个部分的。当每次插入一条记录时,都会从 Free Space(空闲空间)中申请一个记录大小的空间划分到 User Record 部分。当 Free Space 的空间被划分完之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。如图:
在这里插入图片描述

2.4、用户记录【不确定】

用户记录是 InnoDB 的重中之重,平时保存到数据库中的数据,就存储在它里面。

InnoDB 支持以下四种数据行格式:
行格式:就是记录在磁盘上的存放形式或者说存储结构。不同的行格式,存储的结构也不同

  1. Redundant【5.0 之前】:MySQL 5.0 版本之前用的行格式,现在基本不用
  2. Compact【5.1 之后】:在 MySQL 5.0 之后引入,在 MySQL5.1 版本中,默认设置为 Compact 行格式,一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分
  3. Dynamic【5.7 之后】:行格式都和 Compact 挺像,只是在处理溢出列数据和 Compact 不同 ,MySQL5.7 版本之后,默认使用 Dynamic 行格式
  4. Compressed:行格式都和 Compact 挺像,只是在处理溢出列数据和 Compact 不同
    后三个都是紧凑型行格式,为的是存放更多的行记录

这里以 Compact 行格式为例:
在这里插入图片描述

2.1.1、额外信息

额外信息并非真正的用户数据,它是为了辅助存数据用的,并且是逆序存放的。

2.1.1.1、变长字段列表

针对 VARCHAR、TEXT、BLOB 这类变长字段,列中实际存储了多少数据是不固定的,因此除了要把数据本身存下来,还需要记下它的长度【方便按需分配空间】

COMPACT 将变长列的实际长度按照字段的顺序,逆序存储在变长字段长度列表里。

变长字段存储空间分为两部分:真正的数据部分、该数据占用的字节数

如:

CREATE TABLE `demo1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `col1` varchar(45) COLLATE utf8_bin DEFAULT NULL,
  `col2` varchar(45) COLLATE utf8_bin DEFAULT NULL,
  `col3` int(11) DEFAULT NULL,
  `col4` char(5) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=ascii ROW_FORMAT=COMPACT;

并插入三条数据,demo1表中的各个列都使用的是 ascii 字符集(每个字符只需要 1 个字节来进行编码):
在这里插入图片描述

从 demo1 表的第一条记录来看各个字段占用的字节数,因为是变长字段, id、col3(int)、col(char) 这三个字段可以不用管:

  1. clo1 字段是 varchar ,值是 zs,占用两个字节的空间,十六进制 0x02;
  2. clo2 字段是 varchar ,值是 lsa,占用三个字节的空间,十六进制 0x03;

第一行行记录填入变长字段长度列表后的示意图如下:
在这里插入图片描述

逆序排列的目:为了让位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率

2.1.1.2、NULL 值列表

数据库中有些字段的值允许为 NULL,如果把每个字段的 NULL 值,都保存到用户记录中,显然有些浪费存储空间。所以,COMPACT 行格式把一条记录中值为 NULL 的列统一管理起来,存储到 NULL 值列表中。

  1. NULL 值列表是通过 bit 位来进行标识的,一个字段占一个比特位,bit 位按字段逆序排列
  2. 字段值为 NULL 的 bit 位为 1,否则为 0
  3. NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0

NULL 值列表并不是固定的 1 个字节,如果一条记录中有 9 个字段的值都是 NULL,那么 NULL 值列表大小将是两个字节大小,依次类推。

第一条记录:
在这里插入图片描述

2.1.1.3、记录头信息

在这里插入图片描述

它主要包含:

  • deleted_flag:删除标记,用于标记该记录是否被删除了。0 未删除;1 已删除。

执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_flag 标记为 1。


被删除的记录为什么还在页中存储呢?
  这些被删除的记录不会从磁盘上移除,是因为一旦移除,其他的记录还需要在磁盘上重新排列,这会带来性能消耗。所以,只是将这些删除的记录打一个删除标记,以区分正常记录和被删除的记录,所有被删除的记录会组成一个垃圾链表,它们所占用的空间被称为可重用空间,之后如果有新记录插入到表中时,可能会覆盖掉(复用)被删除的记录占用的存储空间

  • min_rec_flag:非叶子节点中的最小记录标记。B+ 树的每层非叶子节点中的最小记录都会添加该标记min_rec_mask 值为 1
  • n_owned:拥有的记录数。页目录中会将所有的记录分成若干个组,每个组中的最后一条记录的头信息中会存储该组一共有多少条记录,来作为 n_owned 字段的值,而其它记录的 n_owned 值都是 0
  • heap_no:堆/页上的位置,它表示当前记录在堆上的位置。用户记录是一条条紧密地排列着,因此称堆
  • record_type:记录类型,其中:0 表示普通记录;1 表示 B+ 树非叶子节点;2 表示 Infrimum 记录; 3 表示 Supremum 记录
  • next_record:下一条记录的位置。表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量

InnoDB 底层规定 Infimum 记录(最小记录)的下一条记录就是当前页中主键值最小的记录,而当前页中主键值最大的记录指向的下一条记录就是 Supremum 记录(最大记录)

2.1.2、隐藏列

数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示:
在这里插入图片描述

  • db_row_id,即行 id,它是一条记录的唯一标识
    • 如果表中有主键,则用主键做行 id
    • 如果表中没有主键,假如有不为 null 的 unique 唯一键,则用它做为行 id
    • 如果表中既没有主键,又没有唯一键,则数据库会自动创建行 id
  • db_trx_id,即事务 id,它是事务的唯一标识
  • db_roll_ptr,即回滚点,它用于事务回滚

2.1.3、真正的数据列

真正的数据列中存储了用户的真实数据,它可以包含很多列的数据

2.1.4、用户记录如何相连

InnoDB:记录额外信息 ==> 记录头信息 ==> 下一条记录的位置

多条用户记录之间通过下一条记录的位置【按照主键从小到大】,组成了一个单向链表。这样就能从前往后,找到所有的记录了

2.5、最大和最小记录

在一个数据页当中,如果存在多条用户记录,它们是通过下一条记录的位置相连的,按照主键值从小到大的顺序形成一个单向链表

不过有个问题:如果才能快速找到最大的记录和最小的记录呢?

这就需要在保存用户记录的同时,也保存最大 Supremum 和最小记录 Infimum 了。

InnoDB 规定的最小记录和最大记录这两条记录的构造:都是由 5 字节大小的记录头信息和 8 字节大小的一个固定的部分组成的 如下图:
在这里插入图片描述

这两条记录其实不是用户自己插入的,而是在生成页的时候默认自动创建的,并称为伪记录或虚拟记录,它们并不存放在 User Record 部分,而是单独存放在 Infimum + Supremum 部分:
在这里插入图片描述

记录头信息中有一个属性:heap_no(记录在堆中的相对位置)。InnoDB 把记录一条一条亲密无间排列的结构称之为堆,其实,此属性就是当前记录在本页中的位置,并把这两条伪记录的值分别设为 0 和 1

2.6、页目录

如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空【但如果仔细想想,检索效率有点低(要对整页用户数据进行扫描)】

更高效的方法:使用页目录。把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是 页目录。每一组的最大记录叫做槽。如下图:
在这里插入图片描述

假设一页的数据分为 4 组,这样在页目录中,就对应了 4 个槽,每个槽中都保存了该组数据的最大值。

这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。如此一来,就能通过二分查找,快速的定位需要查找的记录了。

2.6.1、页目录分组

确定页目录分组的个数:

InnoDB 规定:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1 ~ 8 条之间,剩下的分组中记录的条数范围只能是在 4 ~ 8 条之间

分组的步骤是这样的:

  1. 初始情况下只有两条记录(两个槽):最大记录和最小记录,它们分别属于两个组。
  2. 每当插入一条记录,都会从页目录中找到主键值比本身记录的主键值大,并且差值最小的槽(从本质上来说,槽是一个组内最大的那条记录在页面中的地址偏移量,通过槽可以快速找到对应的主键值),然后把该槽对应记录的 n_owned 值加一(表示本组内又添加了一条记录),直到该组中的记录数等于 8 个。
  3. 当一个组中的记录数等于 8 后,再插入一条记录时,会将组中的记录拆分成两个组,其中一个组中 4 条记录,另一个 5 条记录。这个拆分的过程会在页目录中新增一个槽 ,记录这个新增分组中最大的那条记录的偏移量

假设某表中正常的记录共有 6 条,InnoDB 会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的 5 条记录。分组后:
在这里插入图片描述

再换个角度看,单纯从逻辑上看一下这些记录和页目录的关系:
在这里插入图片描述

最小记录的 n_owned 为 1,而最大记录的 n_owned 的值为 5

2.6.2、页目录分组案例

假如页里共有 18 条记录了(包括最大记录和最小记录),这些记录被分成了 5 组:

在这里插入图片描述

这五个槽位的编号分别为:0、1、2、3、4,所以最初情况下最低的槽就是 low = 0,最高槽就是 high = 4。由于各个槽代表的记录的主键值都是从小到大排序的,所以可以采用二分法来进行快速查找主键值为 6 的记录:

  1. 计算中间槽的位置:(0 + 4)/ 2 = 2,所以查看槽 2 对应记录的主键值为 8,而 8 > 6,所以设置 high = 2,low 保持不变
  2. 重新计算中间槽的位置:(0 + 2)/ 2 = 1,所以查看槽 1 对应记录的主键值为 4,而 4 < 6,所以设置 low = 1,high 保持不变
  3. 因为 high - low 的值为 1,所以确定主键值为 6 的记录在槽 2 对应的组中。沿着槽 2 中主键值所在的单链表中遍历即可得到主键值为 6 的记录

由于每个组中包含的记录的条数只能是 1~8 条,所以遍历一个组中的记录的代价是很小的

在一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法在页目录中确定要查找的主键所在槽位的上一个槽位,并找到该槽所在分组中主键值最大的记录;
  2. 从当前主键值最大的记录开始,通过 next_record 属性往后遍历,直到找到要查找的记录为止。

2.7、页头部【56 字节】

通过上面介绍的内容,数据页之间能够轻松访问了,但剩下还有个比较重要的问题:记录的状态信息。如:一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?

为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是页头部
在这里插入图片描述

  • PAGE_DIRECTION:记录插入的方向。比如:新插入的一条记录的主键值比上一条记录的主键值大,就说这条记录的插入方向是右边;反之则是左边
  • PAGE_N_DIRECTION:一个方向连续插入的记录数量。假设连续几次插入新纪录的方向都是一致的,InnoDB 会把沿着同一个方向插入记录的条数记录下来,这个条数就使用此状态表示。但如果最后一条记录的插入方向改变了的话,这个状态的值是会被清零重新统计的

3、溢出页

varchar 类型最大容量是 65532 B,而 InnoDB 中一个数据页的大小是 16 KB,16 * 1024 = 16386 B,即:一个 varchar 的容量远远大于一个数据页的大小,这样就可能出现一个页存储不下一行记录的情况,这种情况就叫做行溢出。那么,InnoDB 就会单独分配独立于 B+ 树之外的页面来存储这个字段的信息,这样的页称为溢出页。

溢出页:它既不是数据页也不是索引页。这样的字段被称为页外列。不在 B+ 树上,由 B+ 树的行保存溢出页的指针。这样的好处是让 B+ 树的一个节点能容纳更多页,减小树的高度。

在 Compact 和 Redundant 行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据(768 个前缀字节),把剩余的数据分散存储在其它的页中,这叫作分页存储
然后记录的真实数据处用 20 个字节存储指向这些分散页的地址(这 20 个字节中还包括存储了分散在各个页中的真实数据占用的字节数),从而可以找到剩余数据所在的页,这称为页的扩展,如下图所示:
在这里插入图片描述

  • Redundant、Compact :在记录的真实数据处存储一部分数据(存放768个前缀字节),其余的部分存在溢出页
  • Dynamic、Compressed:把所有的数据都存储到溢出页中,只在记录的真实数据处存储指向这些溢出页的地址(20 字节),实际的数据都存放在 Off Page(溢出页)中
    在这里插入图片描述

Compressed 和 Dynamic 行格式的区别:Compressed 行格式在 Dynamic 的基础上优化了一层,存储在其中的行数据会以 zlib 的算法进行压缩,因此对于 BLOB、TEXT、VARCHAR 这类大长度类型的数据能够进行非常有效的存储。

4、Redundant 行格式

在这里插入图片描述

对比 Compact 行格式主要有两大处不同:

  1. Compact 是变长字段长度列表,Redundant 是字段长度偏移列表
  2. Compact 有 NULL 值列表,Redundant 没有 NULL 值列表

4.1、字段长度偏移列表

为什么说 Redundant 行格式会有冗余说法?

因为 Redundant 行格式的字段长度偏移列表会将该行记录中所有列(包括隐藏列)的长度信息都按照逆序存储起来。Redundant 行格式计算列值的长度的方式不像 Compact 行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度

  • 比如:第一行记录的字段长度偏移列表(逆序)是:2B 25 1F 1B 13 0C 06
  • 因为它是按照逆序排列的,所以按照顺序排列就是:06 0C 13 1B 1F 25 2B
  • 可以看出有三个隐藏列和四个字段列

按照两个相邻数值的差值来计算各个字段列值的长度的如下表所示:

在这里插入图片描述

4.2、记录头信息

Redundant 行格式中的记录头信息固定占用 6 个字节(48 位),每位的含义如下:

在这里插入图片描述

与 Compact 行格式的记录头信息对比来看,有两处不同:

  1. Redundant 行格式多了 n_field 和 1byte_offs_flag 这两个属性
  2. Redundant 行格式没有 record_type 这个属性

其中两个属性的含义:

  • n_field:代表一行中列的数量,占用 10 位,所以 MySQL5.0 之前的版本最多只能包含 1023 个列
  • 1 byte_offs_flags:属性定义了字段长度偏移列表占用 1 个字节,还是 2 个字节(使用 127 作为分界点是因为:127 二进制表示为 01111111,第一位为 NULL 比特位,用来标记是否为 NULL)
    • 当记录的真实数据占用的字节数不大于 127 时,占用 1 字节
    • 当记录的真实数据占用的字节数大于 127,但不大于 32767 时,占用 2 字节
    • 当记录的真实数据大于 32767 时,这部分的数据被存放到溢出页中,使用 2 字节来存储梅格列对应的偏移量

4.3、NULL 值处理

因为 Redundant 行格式没有 NULL 值列表,所以在字段长度偏移列表中对各列对应的偏移量做了一些特殊处理:将列对应的偏移量值的第一个比特位作为是否为 NULL 的依据,该比特位也可以称之为 NULL 比特位

在解析一条记录的某个列时,首先看一下该列对应的偏移量的 NULL 比特位是否为 1,如果为 1,那么该列的值就是 NULL,否则就不是 NULL

5、总结

多个数据页之间通过页号构成了双向链表。而每一个数据页的行数据之间,又通过下一条记录的位置构成了单项链表。整体架构图如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值