数据库磁盘文件格式的设计原理内幕

引言

  访问磁盘需通过系统调用来实现,因此通常我们需要指定目标文件的偏移量,然后把数据从磁盘上的形式解析成合适主存的形式。这意味着要想设计一个高效的磁盘数据结构,必须构造一种易于修改和解析的文件格式。在本文中,我们将讨论通用的原理和实践,它们可以帮助我们设计各种磁盘结构,而不仅仅是B树。
  磁盘数据结构中指针管理的语义与内存中的有所不同,你可以将磁盘上的B树看做是一种页管理机制:算法需要组合页并在页中移动。需要计算页和指向它们的指针并将它们放在相应位置。

1 动机

  构建一种文件格式十分类似于在非托管内存模型的语言中构建数据结构。我们先分配一个数据块,然后使用定长的原始数据类型和结构体以设计者希望的方式将其切片;如果想要引用一大块内存或者是变长的数据结构,则可以使用指针。
非托管内存模型的语言允许我们在需要时分配更多的内存,而不必考虑诸如是否有连续的内存段、这些内存是分段的还是连续的、内存释放之后会怎样等问题。而在磁盘上,我们必须自己处理垃圾收集和碎片问题。
  当数据存在于主存时,大部分内存布局的问题都不存在、很容易解决或是能用第三方库来解决。例如,处理变长字段和超大数据就很简单;只要分配内存并使用指针指向它即可,无须以任何特殊的方式对其进行布局。在某些情况下,开发者需要设计出针对应用的特殊内存数据结构以利用CPU缓存行、预取或者硬件等相关特性,但这样做主要是出于优化的目的。

2 二进制编码

  为了将数据结构高效地保存至磁盘中,我们需将它编码成一种紧凑、易于序列化与反序列化的格式。由于不能使用malloc与free等操作,只能使用read和write,所以需要用不同的思路重新思考数据访问的方式,并且据此来准备数据。在本小节中,将讨论构建高效页面布局的主要原理,这些原理适用于任何二进制格式,后续可以利用类似的指导原则来构建文件、序列化格式或通信协议。
在将数据记录组织成页之前,我们首先需要了解这些问题:如何以二进制形式表示键和数据记录,如何将多个值组合成更复杂的结构,以及如何实现可变长度的数据类型和数组。

2.1 原始类型

  键和值具有某种类型,比如int(整数)、date(日期)或string(字符串)等,并且可利用二进制形式表示(序列化为二进制形式和从二进制反序列化)。大多数的数值类型都用固定大小的值表示。当处理多字节数值时,务必在解码时使用相同的字节序,字节序决定了一组字节的先后顺序。
大端 从最高有效字节开始,从高位到低位依次排序。换句话说,最高有效字节具有最低的地址。
小端 从最低有效字节开始,从低位到高位依次排序。

下图解释了大端与小端的差异,对于十六进制的32为整数0xABCDEFGH,其中AB是最高有效字节,以下是分别用大端和小端字节编码的结果。

在这里插入图片描述
  数据记录由数值、字符串、布尔值之类的原始数据以及它们的组合构成。但是,当通过网络传输或者是将其存储到磁盘上时,我们只能使用字节序列。这意味着,当我们发送或写入一条记录时必须先将其序列化(转换成一段可解释的字节序列),当我们发送或读取一条记录时必须先将其反序列化(将字节序列解析为原来的记录)。

2.2 字符串和变长数据

  所有原始数值类型都有固定大小。构造更复杂数值类型类似于C语言中结构体struct,我们可以将任何原始数据类型组合到结构体中,并使用固定长度的数组或者指针指向其他内存区域。
字符串和其他变长数据类型(定长类型数组)可以序列化为一个表示长度的数值字段size,加上size字节(实际数据)。如tuple结构体所示:

typedef struct tuple
{
	size  uint32;
	char  data[size];
}tuple;

该结构具有如下优点:能够在常数时间内获取字符串的长度而无需遍历整个字符串

2.3 功能多样的布尔值、枚举值和标记

  布尔值可以用单个字节表示,也可以将true和false分别表示1和0。由于布尔值只有两个值,用一个完整的字节过于浪费,所以有的开发者常将每8个布尔值合成一批,每个布尔值占一位。我们称1所在的比特位已置位,0所在的比特位未置位。
枚举值可以被表示为整数,常被用于二进制和通信协议。枚举值用于表示重复多、基数少的值。例如用枚举值表示B树节点类型:

enum Node_type
{
	ROOT_NODE, 
	INTERNAL_NODE,
	LEFT_NODE
};

  另一个密切相关的概念是标记,它是打包的布尔值和枚举值的组合。标记可以表示多个非互斥的布尔值参数,例如可以用它表示元组中某个字段是定长还是变长、元组是否被施加锁等信息。由于每个比特位都代表一个标记值,所以我们只能将2的幂用作为掩码。
在这里插入图片描述

像布尔值一样,我们可以利用位掩码和位运算符从打包的值中读写各个标记位。例如,要想将其中某个标记位置为1,可以将它和相应的位掩码求或,也可以用移位运算和位的下标代替位掩码。要想将某一位置为0,可以利用按位与和按位取反运算符。为了测试第n位是否被置位,可以将按位与的结果与0进行比较。
在这里插入图片描述

3 通用原理

  通常,在设计一种文件格式时,首先要确定寻址方式:是否要将文件拆分成为相同大小的页、那些页由单个块或多个连续块所组成。大多数原地更新的存储结构都是用相同大小的页,这么做有利于简化读取和写入访问。仅追加的存储结构通常页写入数据;记录被一条一条地追加上去,一旦内存中该页写满了,就将其刷到磁盘上。
  文件通常以定长的头部开始,可能以一个定长的尾部结束。尾部包含需要被快速访问的辅助信息或解析文件其余部分所必要的信息。文件的其余部分被分成多个页,下图展示了文件的大致组织方式:
在这里插入图片描述
许多数据库的表结构是固定的,其指定了表中字段的数量、顺序和类型。固定的表结构有助于减少存储的数据量:只需使用位置标识符而不用让每条记录都带上字段名。
  构建更复杂的结构常常会涉及层次结构:原始类型构成字段,一组字段构成单元,多个单元构成页,页组成段,段构成区域等等。没有必须遵守的严格规则,一切都取决于设计者为怎怎样的数据创建格式。
数据库文件通常由多个部分组成,并且在该文件的头部、尾部或者另一个单独文件中包含一个查找表,记录了这些部分的起始偏移量。

4 页结构

  数据库将数据记录存储在数据文件和索引文件中。这些文件被划分为固定大小的单元,成为页。页的大小通常是文件系统块的整数倍,一般为4~16KB。
让我们看一个磁盘上B树节点的例子。从数据结构的角度看,在B树中,我们区分叶节点(包含键和数据记录对)与非叶节点(包含键和指向其他节点的指针)。每个B树节点占据一个页或多个链接在一起的页,因此在讨论B树时,“节点”和“页”这几个术语可以互换使用。
下图为定长记录的页组织方式图,每个页是有一系列三元组组成,k表示键,v表示关联值,p表示指向子页的指针。
在这里插入图片描述
这个方案很容易实现,但是有一些缺点:
1)除非是在最右侧,否则在其他位置插入数据都会移动已有的部分数据;
2)无法有效地管理或访问变长记录,只适用于定长的数据。

5 分槽页

  当存储变长记录时,主要问题是如何管理可用空间:已删除记录所占用的空间需要被回收。如果我们把大小为n的记录放进大小为m的记录先前所占用的空间,除非m = n或者剩余空间能恰好被一个 m-n大小的记录填充,否则会出现空余空间。类似地,一个大小为m的段无法放大小为K(K > m)的记录,那这条记录会被插入其他位置,而不会回收这未使用的空间。
  为了简化变长记录的空间管理问题,我们可以将页分成固定大小的段。但在某些情况下还是会浪费一定的空间。例如我们使用64字节的段,那么除非记录长度恰好是64字节的整数倍,否则终将浪费64 - (n % 64)个字节,换句话说,除非记录记录长度是64的整数倍,否则总有一个块仅部分填充。
  空间的回收可以通过简单地重写页并移动记录来完成,但是需要保证记录的偏移量不变,因为页外指针会用到这些偏移量。在做到这一点的同时,我们希望尽可能减小空间的浪费。

为此,迫切需要一种页格式,允许我们:
1)以最小的开销存储变长记录;
2)回收已删除记录占用的空间;
3)引用页中的记录,无论这些记录具体在什么位置。

为高效地存储变长记录,例如字符串、二进制大对象等,我们可以采用一种分页槽的技术,即将页分成若干的槽。这一技术被广泛地应用在许多数据库,如PostgreSQL。
  我们将页组织成一个槽或者单元格的集合。并将指针和单元格分别存放在页的两侧。若想保持记录原有的顺序,我们只需要重新组织指向单元格 的指针。若要删除一条记录,我们只需将记录的指针为空或删除指针即可。
  分槽页具有一个固定大小的页头,包含相关信息:页大小,checksum,页的可用偏移量等信息。而单元格的大小不是固定的,可以容纳任意的数据:键、指针和数据记录等。下图展示了典型的分槽页组织方式,其中每个页都维护了一个区域、单元格和指向它们的指针。
在这里插入图片描述
分槽页的好处:
1)最小开销:分页槽唯一的额外开销是指针数组,它用于记录对应数据在页内的偏移量。
2)空间回收:通过对页进行碎片整理和重写,便可回收空间。
3)动态布局;从页外部,只能通过槽ID来引用槽,具体位置由页内指针决定。

6 单元格布局

  有了标志位、枚举值和原始类型,我们就可以开始设计单元格布局了。之后将单元格组合页,再将页组合树。单元格分为键单元格和键值单元格两种。键单元格包含一个分割键和一个指针,该指针向两个相邻键之间的页。键值单元格包含键和相关联的数据记录。

  我们假定单个页面所有单元格都是统一的(例如,要么全是键单元格,要么全是键值单元格;类似地,要么全都包含定长数据,要么全是包含变长数据,但不能是两者的混合)。这样一来,单元格的元数据只要每个页上保存一份即可,而不用让每个单元格保存一份。

构成一个键单元格需要以下信息:
1)单元格类型
2)键的长度
3)该单元格指向的子页ID
4)键的数据
一个变长键的单元格布局类似如下:
在这里插入图片描述
  我们将定长的字段放在一起,之后是key_size个字节,这有利于简化偏移量的计算,因为所有定长字段都可以通过静态的、预先计算好的偏移量来访问,我们只需为变长数据计算偏移量。

键值单元格存放数据记录而非子页ID。除非以外,它和键单元格的结构类似:
1)单元格类型
2)键的长度
3)值的长度
4)键的数据(以字节表示)
5)数据记录的数据(以字节表示)
在这里插入图片描述
由于页大小是固定的,并且页由页缓存来管理,我们只需要存储页ID即可,使用时在通过查找表将其转换成真正的文件偏移量。单元格偏移量是页局部的概念,它是相对于起始页位置的偏移量,因此可以使用较小的整数基数,使格式更紧凑。

7 将单元格放进分槽页

  要把单元组织成页,按照3.5讨论过的技术。我们将单元格追加至页的右侧,单元格偏移量/指针放置在页的左侧,如图所示:
在这里插入图片描述
  键可以不按顺序插入,其逻辑上的顺序是通过维护偏移量指针的顺序来实现的。这总设计使得向页中追加单元格只需要最低限度的工作量。因为无论是在插入、更新还是删除操作中,始终都不需要移动单元格的位置。

参考:数据库系统内幕,机械工业出版社

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值