** 目录**
干货分享,感谢您的阅读!
在现代数据库管理系统中,数据的存储和组织方式直接影响着应用程序的性能与响应速度。对于 MySQL 的 InnoDB 存储引擎而言,了解其数据行格式的内部结构至关重要,尤其是在进行性能优化和数据完整性管理时。你是否曾经好奇,在你查询数据的背后,数据是如何被巧妙地存储和管理的?本文将深入探讨 InnoDB 数据行格式,重点分析行头信息中各个属性如 delete_mask、min_rec_flag、n_owned、heap_no、record_type 和 next_record 等的含义与作用。
通过这篇文章,我们不仅能够更清楚地理解这些看似复杂的属性如何影响数据的存储和操作,还能为数据库的优化和性能提升提供理论支持。让我们一起揭开这些数据行格式背后的秘密,为我们的数据库管理之旅铺平道路!
一、数据行格式---行头信息回顾
聚焦 InnoDB 行格式,到现在为止一共设计了4种不同类型的行格式 ,分别是 Compact 、 Redundant 、Dynamic 和 Compressed 行格式,随着时间的推移,他们可能会设计出更多的行格式,但是不管怎么变,在原理上大体都是相同的,本次还是针对Compact为背景展开。
(一)整体回顾
Compact 行格式适用于大多数通用场景,尤其是需要高效存储和读取的小型至中型表。它提供了良好的性能和平衡的存储效率,是 InnoDB 存储引擎中的默认选择。
Compact 行格式在物理存储上采用以下结构:
- 行头信息:用于存储事务信息和回滚指针,占用 5 个字节。
- NULL 值位图:用于标识哪些列是 NULL 值,每个列对应 1 个 bit。
- 变长字段长度列表:紧跟在 NULL 值位图之后,记录变长字段的长度信息。
- 隐藏列:每行有 6 个字节用于两个隐藏的系统列,包括事务 ID 和回滚指针。
- 实际数据:存储实际的数据值,紧凑排列。
(二)行头信息再次重申说明
在 InnoDB 存储引擎中,每个记录都有一个记录头信息,它由固定的 5 个字节(40 个二进制位)组成。这 5 个字节中的每一位都有特定的含义,描述了记录的一些重要信息。
每个记录的开头有一个记录头信息,这些信息包含了对记录的描述和控制。以下是每个二进制位代表的详细信息:
- 预留位1(1 bit):该位暂时未被使用。
- 预留位2(1 bit):该位暂时未被使用。
- delete_mask(1 bit):标记该记录是否被删除。如果被删除,则该位为 1;否则为 0。
- min_rec_mask(1 bit):B+树的每层非叶子节点中的最小记录都会添加该标记。如果是最小记录,则该位为 1;否则为 0。
- n_owned(4 bits):表示当前记录拥有的记录数。使用 4 个 bits 来表示,可以表示的最大值为 15。
- heap_no(13 bits):表示当前记录在记录堆中的位置信息。使用 13 个 bits 来表示,可以表示的最大值为 8191。
- record_type(3 bits):表示当前记录的类型。0:普通记录。1:B+树非叶子节点记录。2:最小记录。3:最大记录。
- next_record(16 bits):表示下一条记录相对于当前记录的位置。使用 16 个 bits 来表示,可以表示的最大值为 65535。
这些记录头信息的二进制位提供了有关记录的详细描述,包括了是否被删除、记录的拥有数量、位置信息等。理解这些信息有助于更好地理解 InnoDB 存储引擎中记录的存储和组织方式,以及对数据库的性能和功能的影响。
二、数据行记录头信息数据准备
为了深入理解 InnoDB 表的创建过程和记录头信息的含义,我们创建
demo
表分析
CREATE TABLE demo (
c1 INT,
c2 INT,
c3 VARCHAR(10000),
PRIMARY KEY (c1)
) CHARSET=ascii ROW_FORMAT=Compact;
c1 和 c2 列是用来存储整数的(把 c1 列指定为主键), c3 列是用来存储字符串的,并指定ascii 字符集以及 Compact 的行格式,具体格式如下:
由于将
c1
列指定为主键,InnoDB 不需要为每行数据再额外创建 row_id 列。主键本身就作为数据的唯一标识符。
为了准备数据,我们现在向
demo
表中插入了四条记录:
INSERT INTO demo VALUES
(1, 100, 'aaaa'),
(2, 200, 'bbbb'),
(3, 300, 'cccc'),
(4, 400, 'dddd');
为了简化理解,可以用十进制数来表示记录头信息和实际的列数据。实际上,这些信息是以二进制形式存储的,但用十进制数可以帮助我们直观地理解其结构。此时记录在 页 的 User Records 部分的存储结构如下:
我们开始重点理解头信息中的内容。
三、
delete_mask
属性
在 InnoDB 存储引擎中,每条记录都有一个
delete_mask
属性,这是一个单独的二进制位,用于标记记录的删除状态。
- 当
delete_mask
的值为 0 时,表示记录未被删除; - 当值为 1 时,表示记录已被删除。
当用户执行
DELETE
操作时,InnoDB 并不会立即从磁盘上物理移除该记录,而是将
delete_mask
设置为 1。这意味着记录仍然保留在数据页中,只是在逻辑上被标记为已删除。这种设计避免了频繁的磁盘写操作,从而提高了删除操作的性能。
被逻辑删除的记录会组成一个垃圾链表,这些记录的空间被称为可重用空间。当新记录插入时,InnoDB 可以优先使用这些可重用空间,而不是分配新的空间。这样既提高了空间利用率,又减少了磁盘 I/O 操作。
注意:将
delete_mask
设置为 1 和将记录加入垃圾链表是两个独立的阶段。虽然记录被逻辑删除后仍然占据物理空间,但 InnoDB 可以在之后的事务提交或定期清理任务中进行真正的物理删除操作。这种延迟删除策略确保了数据库系统在处理大量数据删除和插入操作时的高效性和稳定性。
四、
min_rec_flag
标志位
min_rec_flag
标志位用于标记 B+ 树中每层非叶子节点的最小记录,帮助优化查找和遍历操作。只有 B+ 树中每层非叶子节点的最小记录会被设置
min_rec_flag
标志位为 1,其他记录以及叶子节点中的记录,这个标志位都是 0。比如我们之前插入到
demo
表中的记录,它们都是叶子节点记录,
min_rec_flag
都是 0。
为了更好的说明,假设我们有一张表,它的主键索引使用 B+ 树来组织数据。当我们插入记录时,这些记录会作为 B+ 树的叶子节点存储。
在这个简化的 B+ 树中,[1, 3] 是一个叶子节点,[5] 和 [10] 是非叶子节点。 假设 [1, 3] 是叶子节点,其中的 1 是最小记录,但因为它是叶子节点,所以 min_rec_mask
为 0。 而 [5] 作为非叶子节点,其中的最小记录是 5,因此它的 min_rec_mask
为 1。
五、
n_owned
属性
n_owned
是一个元数据字段,表示当前槽记录所拥有的记录数量,即从当前记录开始直到下一个槽记录(或页末尾)的记录数。
在 InnoDB 中,每个数据页的记录按主键顺序排列,并分成若干组。每组记录的第一个记录称为槽记录(slot record),每个槽记录的
n_owned
属性表示它所拥有的记录数目。例如:
Record 1 (Slot) -> Record 2 -> Record 3 (Slot) -> Record 4 -> Record 5
在这个例子中:
Record 1
是一个槽记录,其n_owned
属性值为 2,因为它拥有Record 1
和Record 2
。Record 3
是另一个槽记录,其n_owned
属性值为 3,因为它拥有Record 3
、Record 4
和Record 5
。
这样以来,这个在页内查找数据时用处就很大了。
六、
heap_no
属性
在 InnoDB 存储引擎中,
heap_no
是一个重要的元数据字段,用于标识数据页中记录的位置和顺序。
heap_no
是一个整数,表示记录在数据页中的位置。它唯一标识一个记录在当前页中的顺序,范围从 0 开始递增。
heap_no
主要用于记录的物理定位和排序,在页内的记录管理、遍历和操作中起到关键作用。
每个数据页中有两个特殊的记录,称为伪记录或虚拟记录:
- Infimum 记录:最小记录,
heap_no
为 0,用于表示页的起始。 - Supremum 记录:最大记录,
heap_no
为 1,用于表示页的结束。
这两个伪记录帮助维护页内记录的有序性,并在记录插入、删除和查找时起到辅助作用。
上面向
demo
表中插入了四条记录,其基本结构如下:
正如上面说的每个数据页中包含两个特殊的伪记录,它们的
heap_no
值为 0 和 1,用于标识页的起始和结束。而用户记录的
heap_no
从 2 开始递增,表示它们在页中的位置和顺序。
七、
record_type
属性
record_type
用于区分不同类型的记录,如用户记录、最小记录、最大记录以及临时记录等。根据
record_type
的值,InnoDB 可以执行特定的操作或处理逻辑。
- **
0
(普通记录)**:代表常规的用户记录。这是我们插入到表中的实际数据。 - **
1
(最小记录 - Infimum)**:这是一个特殊的伪记录,用于表示页中的最小记录。它的作用是标记记录链表的起始位置。 - **
2
(最大记录 - Supremum)**:这是另一个特殊的伪记录,用于表示页中的最大记录。它的作用是标记记录链表的结束位置。 - **
3
(临时记录)**:这类记录通常用于一些临时操作或中间状态,具体实现中可能会用到。
按照之前的四条插入数据而言:
Record Type: 1 -> Infimum Record (heap_no: 0)
Record Type: 0 -> User Record 1 (heap_no: 2)
Record Type: 0 -> User Record 2 (heap_no: 3)
Record Type: 0 -> User Record 3 (heap_no: 4)
Record Type: 0 -> User Record 4 (heap_no: 5)
Record Type: 2 -> Supremum Record (heap_no: 1)
在这个例子中:
Infimum
记录和Supremum
记录的record_type
值分别为1
和2
,用于标识页的边界。- 实际用户记录的
record_type
值为0
,表示它们是常规用户数据。
八、
next_record属性
next_record
是一个指针,表示当前记录的下一条记录的相对位置。它是记录头信息的一部分,占用 2 个字节。通过
next_record
字段,InnoDB 可以在数据页内形成记录的单向链表,从而有效地管理和遍历记录。
(一)单向链表形成说明
在 InnoDB 数据页中,记录按照主键值从小到大的顺序排列,并通过
next_record
字段链接在一起,形成一个有序的单向链表。每条记录通过
next_record
指向下一条记录的相对位置(即相对于当前记录的偏移量),使得在页内查找和遍历记录更加高效。
这里有两个特殊记录的
next_record
- Infimum 记录:这是页内的最小记录。它的
next_record
指向本页中主键值最小的用户记录。 - Supremum 记录:这是页内的最大记录。它的
next_record
指向无效位置,但它是本页中主键值最大的用户记录的下一条记录。
以
demo
表中插入的四条记录来看,基本结构如下(注意里面的
record_type是错误的,正确应该是我们上面讲的
):
接着不论对页中的记录进行何种操作,InnoDB 始终会维护一条按照主键值由小到大顺序排列的单链表。这一机制会通过在记录头信息中使用
next_record
指针来实现,确保记录之间的有序链接。
(二)变动中的单向链表
无论是插入、删除还是更新操作,InnoDB 都会动态调整链表中的指针,以维持记录的顺序。具体表现为:
记录插入
当我们向 InnoDB 表中插入记录时,InnoDB 会根据记录的主键值找到适当的位置,将新记录插入到链表中,并更新相邻记录的
next_record
指针。例如:
- 找到插入位置:如果插入的记录主键值为
5
,且页中已有记录主键值为1
,3
,7
,InnoDB 会找到主键值为3
和7
之间的位置。 - 更新指针:将新记录插入后,更新原记录链表中的
next_record
指针。具体来说,主键值为3
的记录的next_record
会指向新记录,新的记录的next_record
会指向主键值为7
的记录。
记录删除
删除记录时,InnoDB 并不会立即移除记录,而是标记为删除(通过
delete_mask
位)。之后,InnoDB 会更新链表中的指针以跳过被删除的记录:
- 标记删除:将记录的
delete_mask
设置为1
。 - 更新指针:修改被删除记录前一条记录的
next_record
,使其直接指向被删除记录的下一条记录,从而跳过被删除的记录。例如,如果删除主键值为5
的记录,主键值为3
的记录的next_record
会指向主键值为7
的记录。
记录更新
更新记录时,如果主键值不变,记录的位置也不变,只需要更新记录的内容。而如果主键值改变,InnoDB 需要删除旧记录并插入新记录:
- 删除旧记录:将原记录标记为删除,并更新链表指针以跳过被删除的记录。
- 插入新记录:根据新主键值找到合适的位置插入新记录,并更新链表中的指针。
案例简单展示
执行删除操作:
DELETE FROM demo WHERE c1 = 2;
InnoDB 并不会立即移除该记录,而是会将记录
2
被标记为删除,将主键值为
1
的记录的
next_record
指针更新为指向主键值为
3
的记录,以跳过被删除的记录。具体如下:
通过删除记录后的示意图,可以看到链表中的指针如何更新以维持链表的顺序和连续性。尽管记录
2
被标记为删除,它的空间可能会被重用,但链表的有序性不会受到影响。
执行插入操作:
INSERT INTO demo VALUES(2, 200, 'bbbb');
InnoDB 会尝试重用之前删除记录
2
的存储空间:InnoDB 发现先前删除的记录
2
的存储空间仍然存在,并且标记为可重用,于是新的记录
2
将写入该可重用空间,链表中的指针将恢复,使新插入的记录
2
重新出现在正确的位置。具体如下:
重新插入主键值为
2
的记录时,InnoDB 重用了之前删除记录
2
的存储空间,并将链表结构恢复到删除前的状态。这样不仅节省了存储空间,还保证了链表的有序性和高效性。
九、总结体会
记录头信息(Record Header Information)包含了描述和控制记录的一系列二进制位。从这次学习来看,我们可以清晰的看出这些基本重要信息起码设计上满足以下三方面的要求:
高效的空间管理:
- Delete Mask:记录是否被删除的标记。删除操作仅是逻辑删除,标记为删除而不立即移除,允许空间被重用,提高了存储空间的利用效率。
- Next Record:记录链表指针,通过维护一个单向链表结构,使得插入、删除和查找操作更为高效。这一机制确保了记录在存储页面中的有序性,并有助于快速查找。
记录之间的关系维护:
- **Heap Number (heap_no)**:记录在页面中的位置,用于记录的排序和检索。Infimum 和 Supremum 虚拟记录确保了页中记录的有序排列,便于遍历和维护。
- **Min Rec Flag (min_rec_flag)**:用于标识 B+树非叶子节点中的最小记录,有助于树结构的维护和节点分裂、合并等操作。
优化查询性能:
- **N Owned (n_owned)**:表示当前记录拥有的后继记录数量,优化了页面中的记录遍历操作,使得查找特定记录时更加高效。
- **Record Type (record_type)**:区分不同类型的记录(普通记录、B+树非叶子节点记录、最小记录、最大记录),有助于优化树结构的操作和维护。
InnoDB 的记录头信息设计不仅是技术实现的细节,更是对数据库存储管理、性能优化、操作高效性等方面的深刻思考的体现。理解这些设计思想,有助于我们更好地掌握 MySQL InnoDB 存储引擎的内部工作原理,并能够在实际应用中更有效地进行数据库优化和问题排查,希望大家可以多体会其重点含义。
主要参考和学习来源
《MySQL 是怎样运行的:从根儿上理解 MySQL》
https://dev.mysql.com/doc/refman/5.7/en/
版权归原作者 张彦峰ZYF 所有, 如有侵权,请联系我们删除。