前言
前面我们已经剖析了mysql中InnoDB
与MyISAM
索引的数据结构,了解了B+树的设计思想、原理,并且介绍了B+树与Hash结构
、平衡二叉树
、AVL树
、B树
等的区别和实际应用场景。
页和页之间并不一定在物理上相连,只是在逻辑上使用双向链表关联。指针、记录究竟是如何存储的呢?其实这就需要联系我们之前提到的行格式了。数据查找在页目录中二分法快速定位到槽,上面的过程都与页的内部结构相关,本文将详细的阐述。
这篇文章将对InnoDB引擎的数据存储结构介绍,带大家熟悉数据库的页的存储结构与行格式,为之后的调优做准备。
1.数据库的存储结构:页
索引实际上是存储在文件上的,确切的说是存储在页结构中的。存储引擎负责数据的读、写操作,不同的存储引擎的数据格式可能不同。本文所介绍的数据库存储结构基于Mysql的InnoDB存储引擎。这也是我们实际工作中所使用的。
1.1 磁盘与内存交互的基本单位:页
InnoDB将数据划分为若干页,Mysql页的默认大小是16kb.可以使用下面的命令进行查看。
在sql server中,页的默认大小是8kb。Oracle中使用块*(Block)的概念来作为磁盘与内存的基本交互单位,块的大小可以是2kb,4kb,8kb,16kb,32kb和64kb。
这里我们强调,mysql中磁盘与内存交互的基本单位是页,这表示我们在磁盘与内存之间进行数据交互,最少是一页,并且每次交互都是整数页。即使我们数据存储只存储了一行,数据库I/O的操作单位也是一页。这样设计其实也是为了提高效率,毕竟I/O的时间消耗很大,不可能读、写一次数据就进行一次磁盘的I/O操作。
1.2 页上层结构
页的上层结构是区,一个区会分配64个连续页,区的大小正好是1MB(16kb * 64)。
段由一个区或多个区组成。段是数据库的分配单位,比如我们创建一个表,就会创建一个表段,创建一个索引,就会创建一个索引段。总而言之,不同类型的数据库对象对应不同类型的段。
这里我们举一个例子,部队中能够与敌军战斗的基本单位是一个兵,就像数据库中内存与磁盘交互的基本单位是一个页。但是一场战争派出去的不可能只是一个兵,而是更大的单位,比如师。对应的,数据库内存分配的最小单位是一个块。
表空间(table space)是一个逻辑容器。表空间中存储的是一个或者多个段,每个段只能属于一个表空间。表空间又可以分为系统表空间,用户表空间,撤销表空间、临时表空间等。
2.页的内部结构
常见的页有数据页(保存b+树节点)、系统页、Undo页和事务数据页。
页可以划分为如下的七个结构。
这七个结构的作用如下。
为了方便大家的理解,我们将他们归纳为三个部分,进行讲解。
2.1 文件头与文件尾
File Header与File Trailer主要是用于描述页的通用信息,用于页与页的交互或者数据的校验等。
- FILE_PAGE_OFFSET:记录页编号,InnoDB可以通过页号唯一确定一个页。
FILE_PAGE_TYPE:代表当前页的类型,比如FILE_PAGE_UNDO_LOG(Undo日志页)、FILE_PAGE_TYPE_SYS(系统页)、FILE_PAGE_INDEX(索引页/数据页)。
FILE_PAGE_PREV与FILE_PAGE_NEXT上一页与下一页。
FILE_PAGE_SAPCE_OR_CHKSUM:校验和,对于一个较长的字符串,我们可以通过算法将其计算得到较短的字符串,即校验和,当我们需要比较两个长的字符串是否相等时,就比较他们校验和即可。同样我们也可以用校验和比较两个页是否相等。
文件尾页同样有校验和,它们是对应的。当我们对磁盘进行数据更新的刷盘执行I/O时,如果由于断电等原因导致数据传输中断,很可能一个页没有操作完。而页是数据库中磁盘与内存交互的基本单位,为了能够保证一致性,我们需要回滚或者将未写完的数据写完。我们如何判断是否写完呢?就是通过校验和。
具体的过程是:在内存向磁盘写入数据时,先更新File Header中的校验和,如果文件未正常写完,头尾校验和会不一致,如果未刷盘的数据有记录,就继续刷完,否则回滚。
FILE_PAGE_LSN:日志序列号,记录文件最后修改时对应的日志序列位置。尾部也有该字段,同样是为了校验页面的完整性。
2.2 记录部分:空闲空间、最大/小记录、用户记录
在最开始,我们会将已存储的数据按照行格式放到User Records,每插入一个数据,就会从Free Space申请一个记录大小的空间划分到User Records,直到Free Space都被转化为User Records空间,会申请一个新的页。User Records中的数据是按照单链表进行连接的。具体细节我们将在介绍行格式的用户头信息时介绍。
在介绍最小记录和最大记录之前,我们先思考一个问题,有必要对记录大小进行比较吗?当然是有必要的,因为我们的节点都是进行排序的。既然需要排序,就需要进行大小的比较了。那么,最大记录和最小记录的作用是什么呢?实际上,它就相当于记录的头、尾节点,这里我们先简单介绍,后面我们介绍完行格式,大家会对这部分理解更为深刻。在mysql中,最小记录与最大记录的格式非常简单,可以参考下图。
2.3 页目录与页头
page_decotory主要是为了方便数据进行二分查找。页目录是不是一开始就生成的呢?比如我们有1000条记录,是不是一开始就按照主键生成这1000条记录的页目录?这样做对于存储的占用显然是很高的。
实际上,mysql数据库做法如下。
将每页所有记录(包括最小记录与最大记录,不包含已删除记录)进行了分组,每个组取一个记录放入页目录中,占一个槽位(slot).最小记录单独为1组,最大记录所在组一般是有1-8条记录,其它组一般有4-8条记录。
每组的最后一条记录会存储一个n_owned属性,代表该组有几个记录。
页目录用于存储每组最后一个记录的地址偏移量。
下图表示了这个过程,其中每个记录的第一个字段就是n_owned.
现在我们已经知道页目录是如何生成的了,那么我们如何利用页目录进行数据的查找呢?实际上就是利用二分法对需要查找的数据与页目录中数据进行比较。比如一个数据确定比槽2的更大,比槽3的更小,那就可以去槽3所链接的分组4进行查找了。它会从槽2所链接的分组3最后一个记录指针链接到分组4的第一个元素,然后进行遍历查找。
接下来我们看看page header
中记录了什么信息。这些信息大概了解下,主要是页的内部结构的一些信息。
其中PAGE_DIRECTION
记录当前新插入记录是需要在上一条记录的前面还是后面插入。
2.4 从存储角度看普通索引和唯一索引有什么不同
我们到目前为止已经了解了页的内部结构和索引的数据结构。接下来我们深入思考一个问题。
普通索引和唯一索引有什么不同?
唯一索引指的是对索引字段加了唯一约束的索引,因此该字段不会重复。我们学习了页的存储结构以后知道,一般一个页默认大小是16kb,可以存放上千条记录,而且这些元素的存放是有序的,元素之间通过指针相连接。因此,普通索引一般只是在找到第一个元素后再多往后进行几次查找即可,其时间消耗并不大(真正消耗时间的是磁盘I/O)。
3.InnoDB行格式
3.1 行格式简介与操作sql
行格式就是记录的存储格式。
行格式一般有Compact
,Dynamic
,Compressed
和Redundant
几种。
可以使用如下的查询语句查询Mysql数据库的默认行格式。在mysql8与mysql5.7中,默认行格式都是dynamic
。
可以使用下列语句查询某个表实际使用的行格式。
使用如下语句可以在创建表时指定其行格式。
修改表的行格式。