MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

说在前面——本篇也是我读书总结笔记,因为是讲底层原理,我个人认为本文难度是相当高的,可能需要一定的基础。

我将文中的图片包含的计算原理全都演算了一遍,因为书本没有过程详细计算,欢迎大家阅读和仔细推敲。

如果是大忙人,也可以跳过计算,看懂过程和大致原理即可。

文章目录

1.记录头信息有什么用?

  记录头信息里面有很多属性,最容易理解的就是next_record指针,单链表都会有next指针,这样才会找得到下一个结点,这对于页中的每条记录也是一样,上一条记录需要知道下一条记录在哪里。

  上一篇说到了innodb行格式,重点讲了一下dynamic行格式,知道一条记录实际存储如下图。这里看不懂没关系,但是必须记住这个图的上面部分,每条记录不仅是记录的真实数据,还有记录的额外信息。
可能你目前只是为了开发学习本部分知识,不懂也没多大关系,接着往下面一节往后看。
MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息
  在utf8mb4字符集中,能用0~4字节表示一个字符,像varchar这种变长类型和char这种定长类型实际占用的字节数都会被记录到变长字段列表

  如果字段没有被NOT NULL限定,那么就允许为NULL,该列就会有NULL值列表

  关于记录头信息,下面这个表先列出来,往后面看的时候不理解时可以返回查看这个表,方便理解。

名称 大小(单位:bit) 描述
预留位1 1 没有使用
预留位2 1 没有使用
delete_mask 1 标记该记录是否被删除
min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 表示当前记录拥有的记录数
heap_no 13 表示当前记录在记录堆的位置信息
record_type 3 表示当前记录的类型,0表示普通记录,1表示B+树非叶节点记录,2表示Infimum记录,3表示Supremum记录
next_record 16 表示本条记录真实数据部分到下一条记录真实数据的距离

从表中所说可以看到,记录头信息一共是40bit就是5个字节

2.记录在页中的存储结构

MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

  innodb管理存储空间的基本单位,一个页的大小默认是16KB,插入的记录会按照指定的行格式(默认dynamic)存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分(也就是尚未使用的存储空间) 申请一个记录大小的空间,并将这个部分划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。

有人会疑问了,图中这个Infimum+Supremum是什么?

   Infimum记录 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录。但是这两条记录不在User Records部分,是单独占用的空间,可结合上一张图理解。都是由5个字节的记录头和8个字节的一个固定单词组成,如下所示
MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

3.记录头信息的底层原理和计算(show time,难度搞起来)

首先,建个表record_test

CREATE TABLE record_test(
 c1 INT,
 c2 INT,
 c3 VARCHAR(10000),
  PRIMARY KEY (c1)
) CHARSET=utf8mb4;

执行插入语句

INSERT INTO record_test VALUES(1, 100, 'aa你好'), (2, 200, 'bb哈哈'), (3, 300, 'cc来了'), (4, 400, 'dd在哪');

最终表数据如下
MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

4条记录的存储结构示意图如下

MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

  看到这里,你一定和我有着相同的疑问,为什么next_record显示36,它表示本条记录真实数据部分到下一条记录真实数据的距离。这个怎么计算的呢?

  现在我来和你说说底层那些不为人知的东西。要知道,记录的真实数据除了所有的数据列之外,MySQL还会为每条记录默认添加一些列(也称为隐藏列),隐藏列也包含在记录的真实数据部分,如下

列名 是否必须 占用空间 描述
DB_ROW_ID 6字节 行ID,唯一标识一条记录
DB_TRX_ID 6字节 事务ID
DB_ROLL_PTR 7字节 回滚指针

  InnoDB表对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique键作为主键(必须NOT NULL不允许存NULL),如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为DB_ROW_ID的隐藏列作为主键。

MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

  从上表中可以看出:InnoDB存储引擎会为每条记录都添加 DB_TRX_IDDB_ROLL_PTR这两个列,但是 DB_ROW_ID是可选的(在没有自定义主键以及不允许存NULL值的Unique键的情况下才会添加该列)。这些隐藏列的值不用我们操心,InnoDB存储引擎会自己帮我们生成的。

所以刚刚next_record36字节的计算方法就是
6+7(隐藏列2个,因为有自定义主键)=13字节
4(int长度)+4(int长度)+8(变长varchar实际字节数)=16字节
下一列记录的额外信息(变长列表+NULL值列表+记录头)
1+1+5=7字节
总共13+16+7=36

如果变长列表不知道怎么计算长度,可以见我前一篇MySQL的varchar水真的太深了——InnoDB记录存储结构

而且你可能会疑问为什么第4条记录的下一条却要-123字节?

  前面说过,最大记录的下一条记录是Supremum记录,而Infimum记录的heap_no0,而Supremum记录的heap_no1,存放位置是在所有记录之前,最小记录的heap_no是从2开始的。前面给大家看过记录在页中的存储结构,知道InfimumSupremum记录在User Records之前。

  所以最大记录的下一条就是要找到Supremum记录,那么就要往回走3条记录和第一条记录的最小记录变长列表+NULL值列表+头信息(共7字节),然后加上Supremum真实数据部分的固定8个字节。

36*3+7+8=123字节

  所以为第4条记录的next_record-123,代表指针往前走123字节就是下一条记录的真实数据部分的地址。


如果你还细致的观察到Infimum记录的next_record28,我觉得你挺适合做研究。

  在存储结构上,Infimum记录后面是Supremum记录,接着才是第一条数据记录。
  逻辑上,Infimum下一条记录是第一条数据记录,所以计算方法是
8(Infimum固定字节) + 5(Supremum记录头) + 8(Supremum固定字节) + 7(第一条数据记录的变长字段列表+NULL值列表+记录头) = 28字节。

  为了更形象的表现next_record的作用,我们用箭头来替代next_record的值,注意箭头指向的位置,都是指向真实数据开始的地址。

MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

你可能会疑问,为啥要next_record指向记录头信息和真实数据之间的位置呢?指向整条记录的开头位置不好吗?

  因为这个位置刚刚好,向左读取就是记录头信息,向右读取就是真实数据。我们前边还说过变长字段长度列表、NULL值列表中的信息都是逆序存放,这样可以使记录中位置靠前的字段和它们对应的字段长度信息在内存中的距离更近,可能会提高高速缓存的命中率。

4.当记录被删除,页中记录存储结构如何变化?

当然最大的疑问就是被删除的记录还在页中么?
  是的,你以为记录删除了,可它还在真实的磁盘上(占用空间依然存在)。这些被删除的记录之所以不从磁盘上移除,是因为移除它们之后,还需要再磁盘中重新排序其他记录,这会带来一定的性能损耗,所以只是打一个删除标记就可以避免这个问题,首先deleted_mask设置为1,然后被删除掉的记录加入到垃圾链表,记录在这个链表中占用的空间称为可重用空间,之后如果有新记录插入到表中的话,它们就可能覆盖掉被删除的这些记录占用的空间。

来演示一下

delete from record_test where c1 = 2;

发现第二条记录被删了
MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息
在内存中是怎么样的呢?

MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

删除第2条记录变化如下

  • 2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1
  • 第2条记录的next_record值变为了0,意味着该记录没有下一条记录了。
  • 第1条记录的next_record指向了第3条记录。
  • 最大记录的n_owned值从5变成了4,因为除了自身Supremum记录外,还有3条数据记录(注:Infimumn_owned1是因为包含自身算一条记录)

  无论怎么对页中的数据进行增删改操作,InnoDB始终会维护记录的一个单向链表,链表中的各个节点是按照主键值从小到大的顺序链接起来的。

5.当删除的记录再次被插入,页中记录存储结构如何变化?

INSERT INTO record_test VALUES(2, 200, 'bb哈哈');

可以看到,刚刚删除的第二条数据又回来了
MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

内存结构变化如下

MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!——底层揭秘MySQL行格式记录头信息

  InnoDB并没有因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。

  当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。


本篇总结:
  本篇主要讲了Infimum+Supremum部分,分别是页中最小记录的前一个和最大记录的后一个记录,User Records部分使我们插入的真实数据部分,Free Space是页总尚未使用的部分。然后讲解了图中next_record指针地址的计算。
  我们知道,页中的记录是单链表,页与页之间是双向链表,其实每个数据页的File Header部分有上一页和下一页的编号,所以所有数据页会组成一个双向链表。


欢迎一键三连~

有问题请留言,大家一起探讨学习

----------------------Talk is cheap, show me the code-----------------------
上一篇:plpgsql系列教程(4.3)-函数返回常用数据类型——记录数据类型


下一篇:542. 矩阵