简单数据库实现——Part8 - B树的叶节点结构

简单数据库实现——Part8 - B树的叶节点结构我们正在将表的结构更改为B树。这是一个很大的变化,需要很多篇文章。本文中,我们将定义叶节点的布局,并支持将键值对插入到单节点树中。首先,我们来回顾一下切换到树形结构的原因。选择表结构使用当前的结构,每个页面只存储行,因此空间利用率很高,我们只在末尾追加,所以插入也很快。但是只能通过扫描整个表来查找特定的行。如果我们想删除一行,必须移动后面所有...
摘要由CSDN通过智能技术生成

简单数据库实现——Part8 - B树的叶节点结构

我们正在将表的结构更改为B树。这是一个很大的变化,需要很多篇文章。本文中,我们将定义叶节点的布局,并支持将键值对插入到单节点树中。首先,我们来回顾一下切换到树形结构的原因。

选择表结构

使用当前的结构,每个页面只存储行,因此空间利用率很高,我们只在末尾追加,所以插入也很快。但是只能通过扫描整个表来查找特定的行。如果我们想删除一行,必须移动后面所有的行。

如果我们将表存储为数组,按ID排序,我们可以用二分来查找特定的ID。但是插入会特别慢,因为我们必须移动很多行来腾出空间。

所以我们要使用树结构。树中的每个节点可以包含可变数量的行,因此我们必须在每个节点中存储一些信息来表示包含的行数。另外,所有不存储任何行的内部节点都有存储开销。我们用更大的数据库文件来换取更快的插入、删除和查找。

未排序数组 排序数组 树结构
页包含 仅数据 仅数据 元数据、key和数据
行每页
插入 O(1) O(n) O(log(n))
删除 O(n) O(n) O(log(n))
查询 O(n) O(log(n)) O(log(n))

节点头部结构

叶节点和内部节点有不同的布局。我们先定义一个enum来区别这两种类型。

+typedef enum {
    NODE_INTERNAL, NODE_LEAF } NodeType;

每个节点对应一个页面。内部节点将通过存储页号来指向它的子节点。B树向寻呼机(pager)询问特定的页码,并获取指向页面缓存的指针。页面按照页码顺序依次存储在数据库文件中。

节点需要在页面开头的头部(header)存储一些元数据(metadata)。这个节点将存储它是什么类型的节点,是否是根节点,以及其父节点的指针。为每个头部(header)的大小和偏移量定义常量:

+/*
+ * Common Node Header Layout
+ */
+const uint32_t NODE_TYPE_SIZE = sizeof(uint8_t);
+const uint32_t NODE_TYPE_OFFSET = 0;
+const uint32_t IS_ROOT_SIZE = sizeof(uint8_t);
+const uint32_t IS_ROOT_OFFSET = NODE_TYPE_SIZE;
+const uint32_t PARENT_POINTER_SIZE = sizeof(uint32_t);
+const uint32_t PARENT_POINTER_OFFSET = IS_ROOT_OFFSET + IS_ROOT_SIZE;
+const uint8_t COMMON_NODE_HEADER_SIZE =
+    NODE_TYPE_SIZE + IS_ROOT_SIZE + PARENT_POINTER_SIZE;

叶节点结构

除了最基础的头部信息,叶节点还需要存储它们包含的键值对个数。

+/*
+ * Leaf Node Header Layout
+ */
+const uint32_t LEAF_NODE_NUM_CELLS_SIZE = sizeof(uint32_t);
+const uint32_t LEAF_NODE_NUM_CELLS_OFFSET = COMMON_NODE_HEADER_SIZE;
+const uint32_t LEAF_NODE_HEADER_SIZE =
+    COMMON_NODE_HEADER_SIZE + LEAF_NODE_NUM_CELLS_SIZE;

叶节点的主体是一个键值对数组,每个键值对都是一个键,后面跟一个值(序列化的行)。

+/*
+ * Leaf Node Body Layout
+ */
+const uint32_t LEAF_NODE_KEY_SIZE = sizeof(uint32_t);
+const uint32_t LEAF_NODE_KEY_OFFSET = 0;
+const uint32_t LEAF_NODE_VALUE_SIZE = ROW_SIZE;
+const uint32_t LEAF_NODE_VALUE_OFFSET =
+    LEAF_NODE_KEY_OFFSET + LEAF_NODE_KEY_SIZE;
+const uint32_t LEAF_NODE_CELL_SIZE = LEAF_NODE_KEY_SIZE + LEAF_NODE_VALUE_SIZE;
+const uint32_t LEAF_NODE_SPACE_FOR_CELLS = PAGE_SIZE - LEAF_NODE_HEADER_SIZE;
+const uint32_t LEAF_NODE_MAX_CELLS =
+    LEAF_NODE_SPACE_FOR_CELLS / LEAF_NODE_CELL_SIZE;

基于这些常量,下面是目前叶节点的布局:

Our leaf node format

在头部为每个布尔值使用整个字节可能会占用一些空间,但这使编写代码来访问这些值更容易。

还要注意的是最后还有一些被浪费的空间,我们在头部之后尽可能多地存储键值对,但是剩余的空间不能容纳整个单元格,我们让它为空,避免节点之间分割数据。

访问叶节点字段

访问键、值和元数据的代码都涉及到刚才定义的常量进行指针运算。

+uint32_t* leaf_node_num_cells(void* node) {
   
+  return node + LEAF_NODE_NUM_CELLS_OFFSET;
+}
+
+void* leaf_node_cell(void* node, uint32_t cell_num) {
   
+  return node + LEAF_NODE_HEADER_SIZE + cell_num * LEAF_NODE_CELL_SIZE;
+}
+
+uint32_t* leaf_node_key(void* node, uint32_t cell_num) {
   
+  return leaf_node_cell(node, cell_num);
+}
+
+void* leaf_node_value(void* node, uint32_t cell_num) {
   
+  return leaf_node_cell(node, cell_num) + LEAF_NODE_KEY_SIZE;
+}
+
+void initialize_leaf_node(void* node) {
    *leaf_node_num_cells(node) = 0; }
+

这些方法返回一个指向相关值的指针,因此他们可以同时用来获取和设置数据。

修改分页器和表

即使每个节点不满,每个节点也会完全占据一页,这意味着我们的分页器(pager)不再需要支持读取/写入部分页面。

-void pager_flush(Pager* pager, uint32_t page_num, uint32_t size) {
   
+void pager_flush(Pager* pager, uint32_t page_num) {
   
   if (pager->pages[page_num] == NULL) {
   
     printf("Tried to flush null page\n");
     exit(EXIT_FAILURE);
@@ -242,7 +337,7 @@ void pager_flush(Pager* pager, uint32_t page_num, uint32_t size) {
   
   }
 
   ssize_t bytes_written =
-      write(pager->file_descriptor, pager->pages[page_num], size);
+      write(pager->file_descriptor, pager->pages[page_num], PAGE_SIZE);
 
   if (bytes_written == -1) {
   
     printf("Error writing: %d\n", errno);
 void db_close(Table* table) {
   
   Pager* pager = table->pager;
-  uint32_t num_full_pages = table->num_rows / ROWS_PER_PAGE;
 
-  for (uint32_t i = 0; i < num_full_pages; i++) {
   
+  for (uint32_t i = 0; i < pager->num_pages; i++) {
   
     if (pager->pages[i] == NULL) {
   
       continue;
     }
-    pager_flush(pager, i, PAGE_SIZE);
+    pager_flush(pager, i);
     free(pager->pages[i]);
     pager->pages[i] = NULL;
   }
 
-  // There may be a partial page to write to the end of the file
-  // This should not be needed after we switch to a B-tree
-  uint32_t num_additional_rows = table->num_rows % ROWS_PER_PAGE;
-  if (num_additional_rows > 0) {
   
-    uint32_t page_num = num_full_pages;
-    if (pager->pages[page_num] != NULL) {
   
-      pager_flush(pager, page_num, num_additional_rows * ROW_SIZE);
-      free(pager->pages[page_num]);
-      pager->pages[page_num] = NULL;
-    }
-  }
-
   int result = close(pager->file_descriptor);
   if (result == -1) {
   
     printf("Error closing db file.\n");

现在,在数据库中存储页面(page)数比存储行(row)数更有意义。页的数量应该与分页器相关联,而不是与表相关联,因为它是数据库使用的页的数量,而不是某个表。B树由它的根节点页号标识,所以表需要跟踪它。

 const uint32_t PAGE_SIZE = 4096;
 const uint32_t TABLE_MAX_PAGES = 100;
-const uint32_t ROWS_PER_PAGE = PAGE_SIZE / ROW_SIZE;
-const uint32_t TABLE_MAX_ROWS = ROWS_PER_PAGE * TABLE_MAX_PAGES;
 
 typedef struct {
   
   int file_descriptor;
   uint32_t file_length;
+  uint32_t num_pages;
   void* pages[TABLE_MAX_PAGES];
 } Pager;
 
 typedef struct {
   
   Pager* pager;
-  uint32_t num_rows;
+  uint32_t root_page_num;
 } Table;
@@ -127,6 +200,10 @@ void* get_page(Pager* pager, uint32_t page_num) {
   
     }
 
     pager->pages[page_num] = page;
+
+    if (page_num >= pager->num_pages) {
   
+      pager->num_pages = page_num + 1;
+    }
   }
 
   return pager->pages[page_num];
@@ -184,6 +269,12 @@ Pager* pager_open(const char* filename) {
   
   Pager* pager = malloc(sizeof(Pager)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值