与格式相比,没有变长字段列表和NULL值列表。 相反,有一个记录所有真实数据的偏移地址表。 偏移地址表按倒序排列,但偏移量的计算仍按正序开始。 从第一个开始,第一个从0开始,累加该字段对应的字节数。 记录头信息中,大部分字段与中相同,但也有很多比较之处。
(记录列数),(字段长度列表中每列占用的字节数),字段缺失。
因为它是MySQL 5.0之前使用的格式,非常古老,使用频率很低,所以这里不做过多解释。
格式
在当前版本的mysql 5.7中,使用的格式是。
除了溢出页的处理之外,基本上是一样的。
在行格式中,对于占用存储空间非常大的列,实际记录的数据处只会存储该列的前768字节数据,剩余的数据会存储在其他几个页中,然后记录的真实数据存储在指向这些页面地址的20个字节中,这样就可以找到剩余数据所在的页面。
这种只将该列的前768字节数据和指向其他页的地址存储在该记录的真实数据中,然后将剩余数据存储在其他页中的情况称为行溢出,存储超过 768 字节的页面也称为溢出页面(blob 页)。
20字节溢出页地址将直接记录在真实数据区域中,而不记录额外的部分数据。
行溢出临界点
MySQL规定一个页中至少要存储两行记录。 简单理解:由于B+树的特性,如果不存储至少2条记录,B+树就没有意义,无法形成有效的索引。
每个页面除了存储我们的记录之外,还需要存储一些附加信息,大约132字节。
每条记录所需的附加信息为 27 字节。 假设一列中存储的数据字节数为n。 如果要保证该列不溢出,需要满足:132 + 2×(27 + n) < 16384。结果是n < 8099。也就是说,如果某列存储的数据较少超过 8099 字节,则该列不会成为溢出列。 如果表中有多个列,则该值较小。
格式
格式会在此基础上进行压缩,尤其是溢出页的压缩。 其中存储的行数据将使用 zlib 算法进行压缩,因此对于 blob 和文本等大长度数据非常有效。 贮存。 但该格式实际上是以时间换空间,性能不友好,不建议在普通业务中使用。
数据页结构
数据页所代表的16KB存储空间可以分为多个部分,不同部分有不同的功能。
姓名
中文名
尺寸
描述
文件
文件头
38字节
页面一般信息
页
页眉
56字节
页面专有信息
+
最小记录和最大记录
26字节
虚拟线路记录
用户
用户记录
不确定
实际存储的行记录内容
可用空间
可用空间
不确定
页面中未使用的空间
页
页面目录
不确定
页面中某些记录的相对位置
文件
文件结尾
8字节
验证页面完整性
每当我们插入一条记录时,我们都会从Free Space部分(即未使用的存储空间)申请一条记录大小的空间,并将其划分到User部分。 当Free Space部分的空间全部被User部分替换时,就表示该页面已经用完。 如果有新的记录插入,则需要申请新的页面。 这个过程的示意图如下:
为了方便告知
先创建一个表:
CREATE TABLE test(
a1 INT,
a2 INT,
a3 VARCHAR(100),
PRIMARY KEY (a1)
) CHARSET=ascii ROW_FORMAT=Compact;
test表中插入几条记录:
INSERT INTO test VALUES(1, 10, 'aaa');
INSERT INTO test VALUES(2, 20, 'bbb');
INSERT INTO test VALUES(3, 30, 'ccc');
INSERT INTO test VALUES(4, 40, 'ddd');
这些记录如下图所示,保存在User中
从图中可以看出,我们的记录按照主键升序形成了一个单链表。
页面(页面目录)
现在我们明白了,页面中的记录按照主键值从小到大的顺序串联成一个单链表。 单链表的特点是插入和删除都很方便,但检索效率不高。 最坏的情况下,还需要遍历链表。 所有节点都可以完成检索。 因此,在页结构中专门设计了页目录模块,为记录创建一个目录,可以通过二分查找的方式进行检索,以提高效率。
1:将所有正常记录(包括最大和最小的记录,不包括标记为已删除的记录)分成几组。
2:每组最后一条记录(即组中最大的记录)的头信息中的属性表示该记录有多少条记录,即组内有多少条记录。
3:分别提取每组最后一条记录的地址偏移量并用于搜索。
注意:此页目录充当主键。
必须要注意的是:
第一:第一组,即最小记录所在的组,只能有1条记录;
第二:最后一组,也就是记录最大的组,只能有1-8条记录;
第三:剩余组的记录数只能在4-8之间;
分组按如下方式进行:
我们再添加8条记录看看效果:
INSERT INTO test VALUES(5, 50, 'eee');
INSERT INTO test VALUES(6, 60, 'fff');
INSERT INTO test VALUES(7, 70, 'ggg');
INSERT INTO test VALUES(8, 80, 'hhh');
INSERT INTO test VALUES(9, 90, 'iii');
INSERT INTO test VALUES(10, 100, 'jjj');
INSERT INTO test VALUES(11, 110, 'kkk');
INSERT INTO test VALUES(12, 120, 'lll');
为了便于理解,图中仅保留了用户记录头信息中的 和 属性。
因为每个槽代表的记录的主键值都是从小到大排序的,所以我们可以使用二分的方法来进行快速查找。
因此,在数据页中查找具有指定主键值的记录的过程分为两步:
1、通过二分法确定该记录所在的槽位,并在该槽位所在的组中找到主键值最大的记录。
2、通过记录的属性遍历slot所在组中的每一条记录。
例如,我们查找主键值为x的记录,计算出中间槽的位置(min+max)/2=mid,并查看中间槽对应的主键值y。 如果xy,max保持不变并且min=mid。 等等等等。
例如:我们要查找主键值为6的记录,过程如下。 计算中间槽位的位置:(0+3)/2=1,所以槽位1对应的记录的主键值为4,因为4 < 6,所以设置low=1,high保持不变。 因为high - low的值为1,所以判断主键值为6的记录在slot 2对应的组中。我们可以很容易得到slot 1对应的记录(主键值为4)。 这条记录的下一条记录是槽2中主键值最小的记录,这条记录的主键值为5。所以我们可以从主键值为5的记录开始,遍历槽中的记录2 查找主键为6的数据。
注意:如果在slot 2的组中找到数据,由于slot 2指向最后一条记录,所以需要向上查找一个slot,找到上一个slot的最后一行,然后向下查找。
文件(文件头)
File是各类页面所共有的,这意味着不同类型的页面都会使用File作为第一个组件。 它描述了一些各个页面共有的信息,比如这个页面的页码是多少? 它的上一页、下一页是谁等等。
每页都有一个单独的页码,就像您的身份证号码一样。 您可以通过页码唯一地定位页面。
和 分别代表本页的上一页和下一页的页码。 这样,很多页面就通过建立双向链表的方式串联起来。
B+树索引
数据页的主要组成部分。 每个数据页可以构成一个双向链表,每个数据页中的记录按照主键值从小到大的顺序构成一个单向链表。 每个数据页都会为其中存储的记录生成一个页目录。 。 通过主键查找记录时,可以使用二进制的方法快速定位到页目录中对应的槽位。
在页面内搜索:
跨多个页面查找:
1:找到记录所在页面。
2:从所在页面找到对应的记录。
在没有索引的情况下,无论我们是根据主键列的值进行搜索,还是根据其他列的值进行搜索,由于我们无法快速定位到记录所在的页面,所以只能从第一页一直往下搜索。双向链表。 按照我们上面讲的搜索方法,在每个页面中找到指定的记录。
指数
同样,我们以上面构建的表测试为例,清除插入的数据。 此时的测试表是一张空数据表。 为了描述方便,我们可以简单理解测试表的行格式如下:
一个简单的索引方案:我们设置一个页面目录,用于根据主键值快速定位记录在页面中的位置。 目录中记录的数据页需要满足下一个数据页中的用户记录的主键值必须大于前一个数据页。 页面中用户记录的主键值。
假设我们每个数据页最多可以存储3条记录(实际上一个数据页很大,可以存储很多条记录)。 这时我们向test表中插入3条记录,数据页就会如下图所示:
test表中插入几条记录:
INSERT INTO test VALUES(1, 10, 'aa');
INSERT INTO test VALUES(2, 20, 'bb');
INSERT INTO test VALUES(4, 40, 'dd');
此时我们将插入另一条记录:
INSERT INTO test VALUES(3, 30, 'cc');
因为按照上面的定义,一个页面最多只能容纳3条记录,所以我们必须分配一个新页面:
第1页的用户记录的主键值最大为4,第2页的一条记录的主键值为3。因为4>3,这不符合用户记录的主键值下一个数据页中的数据必须大于上一页中用户记录的主键值的要求,因此在插入主键值为3的记录时,需要伴随一次记录移动,即将主键值为4的记录移动到第2页,然后将主键值为3的记录插入到第1页。最终形成如图所示。
这个过程称为页面拆分。
在实际的数据存储中,数据页的数量并不是连续的。 当我们向测试表中插入多条记录时,效果可能是这样的:
由于这些16KB的页面在物理存储上可能并不相邻,如果我们想从这么多页面中根据主键值快速定位到某些记录所在的页面,我们需要为它们建立一个目录,并且每个页面页面对应一个目录。 每个目录项由该页中记录的最小主键值和页码组成。 我们为以上页面制作一个目录,如图:
比如我们要查找主键值为5的记录,具体查找过程分为两步:
1:首先根据二分法从目录项中快速判断主键值为5的记录在目录2中(因为4 < 5 < 7),其对应的数据页为第23页。
2:然后按照前面提到的页面查找记录的方法,找到第23页的具体记录。
该目录有一个别名,称为索引。
索引方案在
先前存储用户记录的数据页被重新用于存储目录条目。 为了与用户记录区别,我们将这些用来表示目录项的记录称为目录项记录。
用于区分普通用户记录和目录项记录。
如果表中数据过多,一个数据页不足以存储所有目录项记录,则将使用额外的页来存储目录项记录。 那么如果此时我们插入上图中主键值为10的用户记录:
查询时,我们需要定位存储目录项记录的页面,但这些页面在存储空间中可能并不相邻。 如果我们的表中有很多数据,就会有很多存储目录项记录的页面。 我们如何使用主页来存储目录条目记录? 键值对快速定位存储目录项记录的页面怎么样? 其实也很简单。 为这些存储目录项记录的页面生成一个更高级别的目录,就像多级目录一样。 小目录嵌套在大目录中,实际数据在小目录中,所以现在每个页面的示意图如下所示:
用户记录实际上存储在B+树的最低节点上。 这些节点也称为叶节点或叶子节点。 其余用于存储目录项的节点称为非叶节点或内部节点。 其中,B+树的最顶层节点 该节点也称为根节点。
聚集索引
上面我们介绍的B+树本身就是一个目录,或者说它本身就是一个索引。 它有两个特点:
1:使用记录主键值的大小对记录和页面进行排序
2:B+树的叶子节点存储完整的用户记录。
我们将具有这两个特征的B+树称为聚集索引。 所有完整的用户记录都存储在该聚集索引的叶节点中。 这种聚集索引不需要我们在MySQL语句中显式地使用INDEX语句来创建它。 存储引擎会自动为我们创建聚集索引。 另外一个有趣的点是,在存储引擎中,聚集索引是数据的存储方式(所有用户记录都存储在叶子节点中),也就是所谓的索引即数据、数据即索引。
二级索引
这棵B+树与上面介绍的聚集索引有几个不同之处:
索引的成本
1:空间成本:每次创建索引时,都必须为其构建一棵B+树。 每棵B+树的每个节点都是一个数据页。 默认情况下,一个页面会占用16KB的存储空间。
2:时间成本:每次对表中的数据进行增删改查,都需要修改每个B+树索引。
B+树的每一层节点按照索引列的值从小到大排序,形成双向链表。 无论是叶子节点中的记录还是内部节点中的记录(即无论是用户记录还是目录项记录),它们都按照索引列值从小到大的顺序形成一个单向链表。 增删改操作可能会对节点和记录的顺序造成破坏,因此存储引擎需要额外的时间来执行记录移位、页分割、页回收等操作来维护节点和记录的顺序。
总结
通过分析存储逻辑,我们可以清楚地了解数据在mysql中是如何存储的。 并分析索引树的结构,帮助我们在工作中更合理的使用索引。