Vacuum and Maintenance
到目前为止我们讨论的大部分都是 B-Tree 面向用户的操作。只是这里还有其他的操作在查询是同时发生的,如管理存储的完整性,回收控件,减少开销,以及保持页有序。在后台中处理这些操作让我们可以节约时间并且避免在插入、更新跟删除中产生额外开销。
之前介绍的 Slotted Pages 分槽页需要我们对页进行维护以让其保持良好的状态。比如在一系列对内部节点的分裂跟合并或者对叶子节点的插入、更新跟删除操作可能会导致页有充足的 logical 逻辑空间但却没有足够的 contigunous 连续空间,因此该页面是碎片化的。Figure 4-11 展示了这个状况: 页面仍有一些逻辑空间可用,但他们是碎片化的,被两个已删除的记录及从头部开始的空闲控件分割开。
B-Tree 是从根节点的层级开始往下遍历的,数据记录可以通过跟随从根节点开始,仍 live 存活 (可定位) 的指针下降找到。无法定位的数据记录则需要被垃圾回收: 这些数据记录已经不再被其他节点引用并且已经不会在被读取,所以他们的内容是无效的。
你同样可以在 Figure 4-11 中看到其差别:不像那些已经被删除或重写的区域,其他的节点仍然可以持有指向 Cell 的指针并进行读取。大部分实现都会因为关注性能而不去用 Zero 零数据来填充垃圾区域,因为他们最终还是会被其他的数据重写的。
Fragmentation Caused by Updates and Deletes
接下来考虑在什么情况下页会进入一个奇怪的状态:无法被读取但又需要对其进行压缩。在叶子层级,删除操作只会从 Header 中移除对应 Cell 的偏移量,该 Cell 本身其实仍然存在。在这个操作之后,被删除的 Cell 已经是无法被读取的了,他的内容不会再出现在任何的查询结果中,因此清空其内容或移动邻接的节点是没有必要的。
在页被分裂时,只有偏移量被处理 *(缩短)*了,并且因为页的其余部分都是无法读取的,位于最终偏移量之后的 Cell 也不能再被读取了,所以这些 Cell 最终会被新写入的数据覆盖,或者在 Vacuum 处理中被垃圾回收了。
有一些数据库会依赖于垃圾回收,并将已删除或已被更新过的 Cell 保留在原地来支持多版本并行控制。Cell 在更新完成之前,在并行执行的事务中仍是可以被访问到的,并能在其他线程访问到他们的时候被尽快的回收。有些数据库通过管理一些结构来跟踪这些 ghost 幽灵记录,他们会在能看到他们的事务完成时被尽快的回收。
因为删除只会丢弃 Cell 的偏移量而不会重新移动剩余的 Cell 或是在物理上释放该 Cell 的空间,释放的控件最终可能会散落在页的各个位置。这种情况我们称该页是 fragmented 碎片化的,需要对其进行 defragmentation 碎片整理。
为了执行一个写入,我们通常需要能够匹配对应 Cell 的包含了空闲空间的连续的块。为了将释放的数据段存放在一起来解决这个问题,我们需要重写这整个页。
插入操作将数据组合按照插入的顺序保存,这不会产生什么明显的影响,但自然有序的数据元祖能在顺序读时提供更好的预读支持。
更新通常只会发生在叶子层级的节点上: 内部页的 Key 只会用来作为节点的导航支持跟只用来定义子树的边界。另外,更新会为每个 Key 独立执行,而且一般不会导致树的数据结构发生变化,除非产生了创建溢出页的操作。在叶子层级,更新的操作不会变更 Cell 的顺序跟尝试重写整个页。这意味着 Cell 的多个版本可能会被保存起来,尽管只有一个是可以被定位的。
Page Defragmentation
这个负责回收空间、重写页的操作被称为 compaction 压缩、vacuum 真空或直接是 maintenance 维护。如果没有多余的物理空间了,页的重写可以通过同步的写来实现 (避免创建不必要的溢出页) ,但压缩更多是使用了与之不同的异步处理来遍历所有的页,并对其应用垃圾回收及重写他们的内容。
这个操作通过回收了那些无法访问的 Cell 来清理空间,并按照逻辑顺序来重写 Cell。当页被重写后,Cell 可能会被移动到文件中的不同位置上。内存中未使用的页则会变为可用的并回到页缓存中,磁盘中新的可用的页的 ID 会被添加到 free page list 空闲页列表 (有时也叫 freelist)。这个信息会被持久化到磁盘,保证空闲的空间在节点崩溃或重启时不会丢失。