# 第 10 部分 - 拆分叶子节点

> 原文： [https://cstack.github.io/db_tutorial/parts/part10.html](https://cstack.github.io/db_tutorial/parts/part10.html)

我们的 B-Tree 感觉不像只有一个节点的树。为了解决这个问题，我们需要一些代码来分割一个叶子节点。之后，我们需要创建一个内部节点作为两个叶子节点的父节点。

基本上我们这篇文章的目标是从这个：

![one-node btree](img/5045c4462e09e087b99196ab3e608525.jpg)

单节点 btree

变成：

![two-level btree](img/9b1d283f73bd4b427dee7e63ddf2f106.jpg)

两级 btree

首先，让我们删除完整叶子节点的错误处理：

```diff
 void leaf_node_insert(Cursor* cursor, uint32_t key, Row* value) {
   void* node = get_page(cursor->table->pager, cursor->page_num);

   uint32_t num_cells = *leaf_node_num_cells(node);
   if (num_cells >= LEAF_NODE_MAX_CELLS) {
     // Node full
-    printf("Need to implement splitting a leaf node.\n");
-    exit(EXIT_FAILURE); +    leaf_node_split_and_insert(cursor, key, value);
+    return;
   } 
```

```diff
ExecuteResult execute_insert(Statement* statement, Table* table) {
   void* node = get_page(table->pager, table->root_page_num);
   uint32_t num_cells = (*leaf_node_num_cells(node));
-  if (num_cells >= LEAF_NODE_MAX_CELLS) {
-    return EXECUTE_TABLE_FULL;
-  }

   Row* row_to_insert = &(statement->row_to_insert);
   uint32_t key_to_insert = row_to_insert->id; 
```

## 分割算法

简单的部分结束了。以下是我们需要做的事情的描述，来自 [SQLite 数据库系统：设计与实现](https://play.google.com/store/books/details/Sibsankar_Haldar_SQLite_Database_System_Design_and?id=9Z6IQQnX1JEC&hl=en)

> 如果叶子节点上没有空间，我们将分割驻留在那里的现有条目和新的（被插入）条目分成相等的两半：上半部分和下半部分。 （上半部分的键严格大于下半部分的键。）我们分配一个新的叶子节点，并将上半部分移动到新节点。

让我们获得旧节点的句柄并创建新节点：

```diff
+void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) {
+  /*
+  Create a new node and move half the cells over.
+  Insert the new value in one of the two nodes.
+  Update parent or create a new parent.
+  */
+
+  void* old_node = get_page(cursor->table->pager, cursor->page_num);
+  uint32_t new_page_num = get_unused_page_num(cursor->table->pager);
+  void* new_node = get_page(cursor->table->pager, new_page_num);
+  initialize_leaf_node(new_node); 
```

接下来，将每个单元复制到其新位置：

```diff
+  /*
+  All existing keys plus new key should be divided
+  evenly between old (left) and new (right) nodes.
+  Starting from the right, move each key to correct position.
+  */
+  for (int32_t i = LEAF_NODE_MAX_CELLS; i >= 0; i--) {
+    void* destination_node;
+    if (i >= LEAF_NODE_LEFT_SPLIT_COUNT) {
+      destination_node = new_node;
+    } else {
+      destination_node = old_node;
+    }
+    uint32_t index_within_node = i % LEAF_NODE_LEFT_SPLIT_COUNT;
+    void* destination = leaf_node_cell(destination_node, index_within_node);
+
+    if (i == cursor->cell_num) {
+      serialize_row(value, destination);
+    } else if (i > cursor->cell_num) {
+      memcpy(destination, leaf_node_cell(old_node, i - 1), LEAF_NODE_CELL_SIZE);
+    } else {
+      memcpy(destination, leaf_node_cell(old_node, i), LEAF_NODE_CELL_SIZE);
+    }
+  } 
```

更新每个节点标头中的单元计数：

```diff
+  /* Update cell count on both leaf nodes */
+  *(leaf_node_num_cells(old_node)) = LEAF_NODE_LEFT_SPLIT_COUNT;
+  *(leaf_node_num_cells(new_node)) = LEAF_NODE_RIGHT_SPLIT_COUNT; 
```

然后我们需要更新节点的父节点。如果原始节点是根节点，则它没有父节点。在这种情况下，创建一个新的根节点作为父节点。我现在要将其他分支桩：

```diff
+  if (is_node_root(old_node)) {
+    return create_new_root(cursor->table, new_page_num);
+  } else {
+    printf("Need to implement updating parent after split\n");
+    exit(EXIT_FAILURE);
+  }
+} 
```

## 分配新页面

让我们回过头来定义一些新的函数和常量。当我们创建一个新的叶子节点时，我们将它放在由`get_unused_page_num()`决定的页面中：

```diff
+/*
+Until we start recycling free pages, new pages will always
+go onto the end of the database file
+*/
+uint32_t get_unused_page_num(Pager* pager) { return pager->num_pages; } 
```

目前，我们假设在具有 N 个页面的数据库中，分配了页码 0 到 N-1。因此，我们总是可以为新页面分配页码 N。最终，在我们实现删除后，某些页面可能会变空并且页面编号未使用。为了提高效率，我们可以重新分配这些自由页面。

## 叶子节点大小

为了保持树的平衡，我们在两个新节点之间均匀分配单元。如果叶子节点可以容纳 N 个单元，则在分割期间我们需要在两个节点之间分配 N + 1 个单元（N 个原始单元加上一个新单元）。如果 N + 1 是奇数，我随意选择左节点再获得一个单元。

```diff
+const uint32_t LEAF_NODE_RIGHT_SPLIT_COUNT = (LEAF_NODE_MAX_CELLS + 1) / 2;
+const uint32_t LEAF_NODE_LEFT_SPLIT_COUNT =
+    (LEAF_NODE_MAX_CELLS + 1) - LEAF_NODE_RIGHT_SPLIT_COUNT; 
```

## 创建新根节点

以下是 [SQLite 数据库系统](https://play.google.com/store/books/details/Sibsankar_Haldar_SQLite_Database_System_Design_and?id=9Z6IQQnX1JEC&hl=en)如何解释创建新根节点的过程：

> 设 N 为根节点。首先分配两个节点，比如 L 和 R。将 N 的下半部分移动到 L，将上半部分移动到 R.现在 N 是空的。在 N 中添加`〈L, K,R〉`，其中 K 是 L 中的最大键。页面 N 仍然是根。请注意，树的深度增加了 1，但新树保持高度平衡而不违反任何 B+ 树属性。

在这一点上，我们已经分配了正确的子节点，并将上半部分移入其中。我们的函数将正确的子项作为输入，并分配一个新页面来存储左子项。

```diff
+void create_new_root(Table* table, uint32_t right_child_page_num) {
+  /*
+  Handle splitting the root.
+  Old root copied to new page, becomes left child.
+  Address of right child passed in.
+  Re-initialize root page to contain the new root node.
+  New root node points to two children.
+  */
+
+  void* root = get_page(table->pager, table->root_page_num);
+  void* right_child = get_page(table->pager, right_child_page_num);
+  uint32_t left_child_page_num = get_unused_page_num(table->pager);
+  void* left_child = get_page(table->pager, left_child_page_num); 
```

旧的根节点被复制到左子节点，所以我们可以重用根页面：

```diff
+  /* Left child has data copied from old root */
+  memcpy(left_child, root, PAGE_SIZE);
+  set_node_root(left_child, false); 
```

最后，我们将根页面初始化为具有两个子节点的新内部节点。

```diff
+  /* Root node is a new internal node with one key and two children */
+  initialize_internal_node(root);
+  set_node_root(root, true);
+  *internal_node_num_keys(root) = 1;
+  *internal_node_child(root, 0) = left_child_page_num;
+  uint32_t left_child_max_key = get_node_max_key(left_child);
+  *internal_node_key(root, 0) = left_child_max_key;
+  *internal_node_right_child(root) = right_child_page_num;
+} 
```

## 内部节点格式

现在我们终于创建了一个内部节点，我们必须定义它的布局。它从公共标头开始，然后是它包含的键数，然后是最右边的子页的页码。内部节点的子指针总是比它们的键多一。额外的子指针存储在标头中。

```diff
+/*
+ * Internal Node Header Layout
+ */
+const uint32_t INTERNAL_NODE_NUM_KEYS_SIZE = sizeof(uint32_t);
+const uint32_t INTERNAL_NODE_NUM_KEYS_OFFSET = COMMON_NODE_HEADER_SIZE;
+const uint32_t INTERNAL_NODE_RIGHT_CHILD_SIZE = sizeof(uint32_t);
+const uint32_t INTERNAL_NODE_RIGHT_CHILD_OFFSET =
+    INTERNAL_NODE_NUM_KEYS_OFFSET + INTERNAL_NODE_NUM_KEYS_SIZE;
+const uint32_t INTERNAL_NODE_HEADER_SIZE = COMMON_NODE_HEADER_SIZE +
+                                           INTERNAL_NODE_NUM_KEYS_SIZE +
+                                           INTERNAL_NODE_RIGHT_CHILD_SIZE; 
```

主体是一个单元数组，其中每个单元包含一个子指针和一个键。每个键应该是左侧子项中包含的最大键。

```diff
+/*
+ * Internal Node Body Layout
+ */
+const uint32_t INTERNAL_NODE_KEY_SIZE = sizeof(uint32_t);
+const uint32_t INTERNAL_NODE_CHILD_SIZE = sizeof(uint32_t);
+const uint32_t INTERNAL_NODE_CELL_SIZE =
+    INTERNAL_NODE_CHILD_SIZE + INTERNAL_NODE_KEY_SIZE; 
```

基于这些常量，以下是内部节点的布局：

![Our internal node format](img/47bd60c50ee719eebddd8af482c78fc9.jpg)

我们的内部节点格式

注意我们巨大的分支因子。因为每个子指针/键对都很小，所以我们可以在每个内部节点中容纳 510 个键和 511 个子指针。这意味着我们永远不必遍历树的许多层来找到给定的键！

| 内部节点层数 | 叶子节点最大值 | 所有叶子节点的大小 |
| --- | --- | --- |
| 0 | 511 ^ 0 = 1 | 4 KB |
| 1 | 511 ^ 1 = 512 | ~2 MB |
| 2 | 511 ^ 2 = 261,121 | ~1 GB |
| 3 | 511 ^ 3 = 133,432,831 | ~550 GB |

实际上，由于标头，键和浪费空间的开销，我们无法为每个叶子节点存储完整的 4 KB 数据。但是我们可以通过从磁盘加载仅 4 页来搜索 500 GB 的数据。这就是 B-Tree 是数据库的有用数据结构的原因。

以下是读取和写入内部节点的方法：

```diff
+uint32_t* internal_node_num_keys(void* node) {
+  return node + INTERNAL_NODE_NUM_KEYS_OFFSET;
+}
+
+uint32_t* internal_node_right_child(void* node) {
+  return node + INTERNAL_NODE_RIGHT_CHILD_OFFSET;
+}
+
+uint32_t* internal_node_cell(void* node, uint32_t cell_num) {
+  return node + INTERNAL_NODE_HEADER_SIZE + cell_num * INTERNAL_NODE_CELL_SIZE;
+}
+
+uint32_t* internal_node_child(void* node, uint32_t child_num) {
+  uint32_t num_keys = *internal_node_num_keys(node);
+  if (child_num > num_keys) {
+    printf("Tried to access child_num %d > num_keys %d\n", child_num, num_keys);
+    exit(EXIT_FAILURE);
+  } else if (child_num == num_keys) {
+    return internal_node_right_child(node);
+  } else {
+    return internal_node_cell(node, child_num);
+  }
+}
+
+uint32_t* internal_node_key(void* node, uint32_t key_num) {
+  return internal_node_cell(node, key_num) + INTERNAL_NODE_CHILD_SIZE;
+} 
```

对于内部节点，最大键始终是其右键。对于叶子节点，它是最大索引处的键：

```diff
+uint32_t get_node_max_key(void* node) {
+  switch (get_node_type(node)) {
+    case NODE_INTERNAL:
+      return *internal_node_key(node, *internal_node_num_keys(node) - 1);
+    case NODE_LEAF:
+      return *leaf_node_key(node, *leaf_node_num_cells(node) - 1);
+  }
+} 
```

## 跟踪根节点

我们最终使用公共节点头中的`is_root`字段。回想一下，我们使用它来决定如何拆分叶子节点：

```c
 if (is_node_root(old_node)) {
    return create_new_root(cursor->table, new_page_num);
  } else {
    printf("Need to implement updating parent after split\n");
    exit(EXIT_FAILURE);
  }
} 
```

以下是 getter 和 setter：

```diff
+bool is_node_root(void* node) {
+  uint8_t value = *((uint8_t*)(node + IS_ROOT_OFFSET));
+  return (bool)value;
+}
+
+void set_node_root(void* node, bool is_root) {
+  uint8_t value = is_root;
+  *((uint8_t*)(node + IS_ROOT_OFFSET)) = value;
+} 
```

两种类型的节点的初始化应默认将`is_root`设置为`false`：

```diff
 void initialize_leaf_node(void* node) {
   set_node_type(node, NODE_LEAF);
+  set_node_root(node, false);
   *leaf_node_num_cells(node) = 0;
 }

+void initialize_internal_node(void* node) {
+  set_node_type(node, NODE_INTERNAL);
+  set_node_root(node, false);
+  *internal_node_num_keys(node) = 0;
+}
```

在创建表的第一个节点时，我们应该将`is_root`设置为 true：

```diff
 // New database file. Initialize page 0 as leaf node.
     void* root_node = get_page(pager, 0);
     initialize_leaf_node(root_node);
+    set_node_root(root_node, true);
   }

   return table; 
```

## 打印树

为了帮助我们可视化数据库的状态，我们应该更新`.btree`元命令来打印多级树。

我要替换当前的`print_leaf_node()`函数

```diff
-void print_leaf_node(void* node) {
-  uint32_t num_cells = *leaf_node_num_cells(node);
-  printf("leaf (size %d)\n", num_cells);
-  for (uint32_t i = 0; i < num_cells; i++) {
-    uint32_t key = *leaf_node_key(node, i);
-    printf("  - %d : %d\n", i, key);
-  }
-} 
```

使用一个新的递归函数，它接受任何节点，然后打印它及其子节点。它将缩进级别作为参数，随着每次递归调用而增加。我还添加了一个小的辅助函数来缩进。

```diff
+void indent(uint32_t level) {
+  for (uint32_t i = 0; i < level; i++) {
+    printf("  ");
+  }
+}
+
+void print_tree(Pager* pager, uint32_t page_num, uint32_t indentation_level) {
+  void* node = get_page(pager, page_num);
+  uint32_t num_keys, child;
+
+  switch (get_node_type(node)) {
+    case (NODE_LEAF):
+      num_keys = *leaf_node_num_cells(node);
+      indent(indentation_level);
+      printf("- leaf (size %d)\n", num_keys);
+      for (uint32_t i = 0; i < num_keys; i++) {
+        indent(indentation_level + 1);
+        printf("- %d\n", *leaf_node_key(node, i));
+      }
+      break;
+    case (NODE_INTERNAL):
+      num_keys = *internal_node_num_keys(node);
+      indent(indentation_level);
+      printf("- internal (size %d)\n", num_keys);
+      for (uint32_t i = 0; i < num_keys; i++) {
+        child = *internal_node_child(node, i);
+        print_tree(pager, child, indentation_level + 1);
+
+        indent(indentation_level);
+        printf("- key %d\n", *internal_node_key(node, i));
+      }
+      child = *internal_node_right_child(node);
+      print_tree(pager, child, indentation_level + 1);
+      break;
+  }
+} 
```

并更新对`print`函数的调用，将缩进级别传递为零。

```diff
 } else if (strcmp(input_buffer->buffer, ".btree") == 0) {
     printf("Tree:\n");
-    print_leaf_node(get_page(table->pager, 0)); +    print_tree(table->pager, 0, 0);
     return META_COMMAND_SUCCESS; 
```

这是新的打印函数的测试用例！

```diff
+  it 'allows printing out the structure of a 3-leaf-node btree' do
+    script = (1..14).map do |i|
+      "insert #{i} user#{i} person#{i}@example.com"
+    end
+    script << ".btree"
+    script << "insert 15 user15 person15@example.com"
+    script << ".exit"
+    result = run_script(script)
+
+    expect(result[14...(result.length)]).to match_array([
+      "db > Tree:",
+      "- internal (size 1)",
+      "  - leaf (size 7)",
+      "    - 1",
+      "    - 2",
+      "    - 3",
+      "    - 4",
+      "    - 5",
+      "    - 6",
+      "    - 7",
+      "- key 7",
+      "  - leaf (size 7)",
+      "    - 8",
+      "    - 9",
+      "    - 10",
+      "    - 11",
+      "    - 12",
+      "    - 13",
+      "    - 14",
+      "db > Need to implement searching an internal node",
+    ])
+  end 
```

新格式有点简化，因此我们需要更新现有的`.btree`测试：

```diff
 "db > Executed.",
       "db > Executed.",
       "db > Tree:",
-      "leaf (size 3)",
-      "  - 0 : 1",
-      "  - 1 : 2",
-      "  - 2 : 3", +      "- leaf (size 3)",
+      "  - 1",
+      "  - 2",
+      "  - 3",
       "db > "
     ])
   end 
```

这是新测试的`.btree`输出：

```
Tree:
- internal (size 1)
  - leaf (size 7)
    - 1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
  - key 7
  - leaf (size 7)
    - 8
    - 9
    - 10
    - 11
    - 12
    - 13
    - 14 
```

在最小缩进级别，我们看到根节点（内部节点）。它说`size 1`因为它有一个键。缩进为一，我们看到一个叶子节点，一个键和另一个叶子节点。根节点（7）中的键是第一个叶子节点中的最大键。每个大于 7 的键都在第二个叶子节点中。

## 一个主要问题

如果你一直密切关注，你可能会注意到我们错过了一些大事。看看如果我们尝试插入另一行会发生什么：

```
db > insert 15 user15 person15@example.com
Need to implement searching an internal node 
```

我了个去！谁写了 TODO 消息？:P

下次我们将通过在多级树上实现搜索来继续史诗级 B 树之旅。