在 MYSQL 中,数据记录、索引信息都是保存在文件上的,确切地说,是保存在页结构中。
# 查看 MYSQL 数据存储目录
SHOW VARIABLES LIKE 'datadir';
SELECT @@datadir
执行之后,如图:
InnoDB 存储引擎创建的任何一张表的都会有两个文件:
为什么要引入数据页?
所以,InnoDB 采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位
程序局部性原理:查询数据库时,不论读一行,还是读多行,都是将这些行所在的整页数据加载,然后在内存中匹配过滤出最终结果;表的检索速度跟树的深度有直接关系,毕竟一次页加载就是一次 IO,而磁盘 IO 又是比较费时间
数据页主要是用来存储表中记录的,它在磁盘中是用双向链表(支持 order by id asc 和 order by id desc )相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页(表中的数据比较多,在磁盘中可能存放在多个数据页)
要根据某个条件查询数据时,需要从一个数据页找到另一个数据页,这时候的双向链表就派上大用场了。磁盘中各数据页的整体结构如下图所示:
通常情况下,单个数据页默认的大小是 16kb。当然,我们也可以通过参数:innodb_page_size,来重新设置大小。不过,一般情况下,用它的默认值就够了
数据页是 InnoDB 存储引擎磁盘管理的最小单元,数据库每次读写都是以【页】为单位的,一次最少从磁盘中读取 16KB 的内容到内存中
InnoDB 存储引擎的逻辑存储结构如图:
从 InnoDB 存储引擎的逻辑存储结构看:所有数据都被逻辑地存放在一个空间中,称之为表空间( tablespace)。表空间又由段(segment)、区( extent)、页(page)组成。页在一些文档中有时也称为块( block)。
表空间表示一本书,段表示书中的章节,区表示每章节的小节,页表示书的每一页,行就是每页的每行数据。表空间里有多个段,一个段包含 256 个区,一个区包含 64 个页,一个页为 16 KB
在 InnoDB 存储引擎中,数据是按照表空间来组织存储的。表空间是表空间文件是实际存在的物理文件。
当我们创建一个表之后,在磁盘上会有对应的表名称 .ibd 的磁盘文件,这个文件就是这张表的表空间物理文件
数据页结构图如下:
InnoDB 能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中
如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?
这时就需要使用文件头部了。它里面包含了多个信息。如下:
其中,我们需要重点关注一下标黄的属性:
InnoDB 是通过页号、上一页页号和下一页页号来串联不同数据页的【这些页与页之间在逻辑上连接而非物理连接】。如下图所示:
⭐ 什么是校验和?
就类似于一个很长的字符串,通过某种算法计算出一个比较短的值来代表这个字符串,这个比较短的值就是校验和。
⭐ 为什么要使用校验和?
在比较两个很长的字符串时,如果直接进行比较的话,肯定会比较慢的。但是,如果通过比较两个字符串的校验和(其中,生成校验和耗时可以忽略不计):校验和相同就代表两个字符串相同,反之则不同。这种方式很明显会缩短比较时的耗时。
⭐ 校验和在页面上有什么作用?
校验和这个属性是存在于文件头部和文件尾部的。InnoDB 以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间就需要把数据同步刷新到磁盘中了。但是同步时可能会出现断电、磁盘损坏等一系列不可抗因素,从而造成数据页传输的不完整。
为了检测一个页是否刷盘成功,就可以通过文件头部的校验和与文件尾部的校验和做对比,如果两个值不相等则说明页刷盘失败,需要重新刷盘(数据重试或回滚操作);否则认为页刷盘成功。
当我们存储一条数据的时候,存储的记录会按照指定的行格式存储到 User Records 部分。但是在最开始生成页的时候,其实是没有 User Record 这个部分的。当每次插入一条记录时,都会从 Free Space(空闲空间)中申请一个记录大小的空间划分到 User Record 部分。当 Free Space 的空间被划分完之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。如图:
用户记录是 InnoDB 的重中之重,平时保存到数据库中的数据,就存储在它里面。
InnoDB 支持以下四种数据行格式:
行格式:就是记录在磁盘上的存放形式或者说存储结构。不同的行格式,存储的结构也不同
这里以 Compact 行格式为例:
额外信息并非真正的用户数据,它是为了辅助存数据用的,并且是逆序存放的。
针对 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) 这三个字段可以不用管:
第一行行记录填入变长字段长度列表后的示意图如下:
逆序排列的目:为了让位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率
数据库中有些字段的值允许为 NULL,如果把每个字段的 NULL 值,都保存到用户记录中,显然有些浪费存储空间。所以,COMPACT 行格式把一条记录中值为 NULL 的列统一管理起来,存储到 NULL 值列表中。
NULL 值列表并不是固定的 1 个字节,如果一条记录中有 9 个字段的值都是 NULL,那么 NULL 值列表大小将是两个字节大小,依次类推。
第一条记录:
它主要包含:
执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_flag 标记为 1。
被删除的记录为什么还在页中存储呢?
这些被删除的记录不会从磁盘上移除,是因为一旦移除,其他的记录还需要在磁盘上重新排列,这会带来性能消耗。所以,只是将这些删除的记录打一个删除标记,以区分正常记录和被删除的记录,所有被删除的记录会组成一个垃圾链表,它们所占用的空间被称为可重用空间,之后如果有新记录插入到表中时,可能会覆盖掉(复用)被删除的记录占用的存储空间
InnoDB 底层规定 Infimum 记录(最小记录)的下一条记录就是当前页中主键值最小的记录,而当前页中主键值最大的记录指向的下一条记录就是 Supremum 记录(最大记录)
数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示:
真正的数据列中存储了用户的真实数据,它可以包含很多列的数据
InnoDB:记录额外信息 ==> 记录头信息 ==> 下一条记录的位置
多条用户记录之间通过下一条记录的位置【按照主键从小到大】,组成了一个单向链表。这样就能从前往后,找到所有的记录了
在一个数据页当中,如果存在多条用户记录,它们是通过下一条记录的位置相连的,按照主键值从小到大的顺序形成一个单向链表
不过有个问题:如果才能快速找到最大的记录和最小的记录呢?
这就需要在保存用户记录的同时,也保存最大 Supremum 和最小记录 Infimum 了。
InnoDB 规定的最小记录和最大记录这两条记录的构造:都是由 5 字节大小的记录头信息和 8 字节大小的一个固定的部分组成的。 如下图:
这两条记录其实不是用户自己插入的,而是在生成页的时候默认自动创建的,并称为伪记录或虚拟记录,它们并不存放在 User Record 部分,而是单独存放在 Infimum + Supremum 部分:
记录头信息中有一个属性:heap_no(记录在堆中的相对位置)。InnoDB 把记录一条一条亲密无间排列的结构称之为堆,其实,此属性就是当前记录在本页中的位置,并把这两条伪记录的值分别设为 0 和 1
如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空【但如果仔细想想,检索效率有点低(要对整页用户数据进行扫描)】
这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。如此一来,就能通过二分查找,快速的定位需要查找的记录了。
InnoDB 规定:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1 ~ 8 条之间,剩下的分组中记录的条数范围只能是在 4 ~ 8 条之间
分组的步骤是这样的:
假设某表中正常的记录共有 6 条,InnoDB 会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的 5 条记录。分组后:
最小记录的 n_owned 为 1,而最大记录的 n_owned 的值为 5
假如页里共有 18 条记录了(包括最大记录和最小记录),这些记录被分成了 5 组:
这五个槽位的编号分别为:0、1、2、3、4,所以最初情况下最低的槽就是 low = 0,最高槽就是 high = 4。由于各个槽代表的记录的主键值都是从小到大排序的,所以可以采用二分法来进行快速查找主键值为 6 的记录:
由于每个组中包含的记录的条数只能是 1~8 条,所以遍历一个组中的记录的代价是很小的
在一个数据页中查找指定主键值的记录的过程分为两步:
为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是页头部
varchar 类型最大容量是 65532 B,而 InnoDB 中一个数据页的大小是 16 KB,16 * 1024 = 16386 B,即:一个 varchar 的容量远远大于一个数据页的大小,这样就可能出现一个页存储不下一行记录的情况,这种情况就叫做行溢出。那么,InnoDB 就会单独分配独立于 B+ 树之外的页面来存储这个字段的信息,这样的页称为溢出页。
溢出页:它既不是数据页也不是索引页。这样的字段被称为页外列。不在 B+ 树上,由 B+ 树的行保存溢出页的指针。这样的好处是让 B+ 树的一个节点能容纳更多页,减小树的高度。
Compressed 和 Dynamic 行格式的区别:Compressed 行格式在 Dynamic 的基础上优化了一层,存储在其中的行数据会以 zlib 的算法进行压缩,因此对于 BLOB、TEXT、VARCHAR 这类大长度类型的数据能够进行非常有效的存储。
对比 Compact 行格式主要有两大处不同:
为什么说 Redundant 行格式会有冗余说法?
因为 Redundant 行格式的字段长度偏移列表会将该行记录中所有列(包括隐藏列)的长度信息都按照逆序存储起来。Redundant 行格式计算列值的长度的方式不像 Compact 行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度
- 比如:第一行记录的字段长度偏移列表(逆序)是:2B 25 1F 1B 13 0C 06
- 因为它是按照逆序排列的,所以按照顺序排列就是:06 0C 13 1B 1F 25 2B
- 可以看出有三个隐藏列和四个字段列
按照两个相邻数值的差值来计算各个字段列值的长度的如下表所示:
Redundant 行格式中的记录头信息固定占用 6 个字节(48 位),每位的含义如下:
与 Compact 行格式的记录头信息对比来看,有两处不同:
其中两个属性的含义:
因为 Redundant 行格式没有 NULL 值列表,所以在字段长度偏移列表中对各列对应的偏移量做了一些特殊处理:将列对应的偏移量值的第一个比特位作为是否为 NULL 的依据,该比特位也可以称之为 NULL 比特位
在解析一条记录的某个列时,首先看一下该列对应的偏移量的 NULL 比特位是否为 1,如果为 1,那么该列的值就是 NULL,否则就不是 NULL
多个数据页之间通过页号构成了双向链表。而每一个数据页的行数据之间,又通过下一条记录的位置构成了单项链表。整体架构图如下:
因篇幅问题不能全部显示,请点此查看更多更全内容