# 第 13 部分 - 拆分后更新父节点

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

对于我们史诗级 b 树实现过程的下一步，我们将在分割叶子后修复父节点。我将使用以下示例作为参考：

![Example of updating internal node](img/c5dad3580c003c98c3b7e76848e501a9.jpg)

更新内部节点的示例

在此示例中，我们将键“3”添加到树中。这会导致左叶子节点分裂。在拆分之后，我们通过执行以下操作来修复树：

1.  将父项中的第一个键更新为左子项中的最大键（“3”）
2.  在更新的键之后添加新的子指针/键对
    *   新指针指向新的子节点
    *   新键是新子节点中的最大键（“5”）

首先，首先用两个新的函数调用替换我们的桩代码：步骤 1 的`update_internal_node_key()`和步骤 2 的`internal_node_insert()`

```diff
@@ -670,9 +725,11 @@ void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) {
   */

   void* old_node = get_page(cursor->table->pager, cursor->page_num);
+  uint32_t old_max = get_node_max_key(old_node);
   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);
+  *node_parent(new_node) = *node_parent(old_node);
   *leaf_node_next_leaf(new_node) = *leaf_node_next_leaf(old_node);
   *leaf_node_next_leaf(old_node) = new_page_num;

@@ -709,8 +766,12 @@ void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) {
   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); +    uint32_t parent_page_num = *node_parent(old_node);
+    uint32_t new_max = get_node_max_key(old_node);
+    void* parent = get_page(cursor->table->pager, parent_page_num);
+
+    update_internal_node_key(parent, old_max, new_max);
+    internal_node_insert(cursor->table, parent_page_num, new_page_num);
+    return;
   }
 } 
```

为了获得对父节点的引用，我们需要在每个节点中开始记录指向其父节点的指针。

```diff
+uint32_t* node_parent(void* node) { return node + PARENT_POINTER_OFFSET; } 
```

```diff
@@ -660,6 +675,48 @@ void create_new_root(Table* table, uint32_t right_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;
+  *node_parent(left_child) = table->root_page_num;
+  *node_parent(right_child) = table->root_page_num;
 } 
```

现在我们需要在父节点中找到受影响的单元。子节点不知道自己的页码，所以我们无法找到它。但它确实知道自己的最大键，因此我们可以在父项中搜索该键。

```diff
+void update_internal_node_key(void* node, uint32_t old_key, uint32_t new_key) {
+  uint32_t old_child_index = internal_node_find_child(node, old_key);
+  *internal_node_key(node, old_child_index) = new_key;
 } 
```

在`internal_node_find_child()`内部，我们将重用一些我们已经拥有的代码来查找内部节点中的键。重构`internal_node_find()`来使用新的辅助方法。

```diff
-Cursor* internal_node_find(Table* table, uint32_t page_num, uint32_t key) {
-  void* node = get_page(table->pager, page_num); +uint32_t internal_node_find_child(void* node, uint32_t key) {
+  /*
+  Return the index of the child which should contain
+  the given key.
+  */
+
   uint32_t num_keys = *internal_node_num_keys(node);

-  /* Binary search to find index of child to search */ +  /* Binary search */
   uint32_t min_index = 0;
   uint32_t max_index = num_keys; /* there is one more child than key */

@@ -386,7 +394,14 @@ Cursor* internal_node_find(Table* table, uint32_t page_num, uint32_t key) {
     }
   }

-  uint32_t child_num = *internal_node_child(node, min_index); +  return min_index;
+}
+
+Cursor* internal_node_find(Table* table, uint32_t page_num, uint32_t key) {
+  void* node = get_page(table->pager, page_num);
+
+  uint32_t child_index = internal_node_find_child(node, key);
+  uint32_t child_num = *internal_node_child(node, child_index);
   void* child = get_page(table->pager, child_num);
   switch (get_node_type(child)) {
     case NODE_LEAF: 
```

现在我们来看看本文的核心，实现`internal_node_insert()`。我会拆分它。

```diff
+void internal_node_insert(Table* table, uint32_t parent_page_num,
+                          uint32_t child_page_num) {
+  /*
+  Add a new child/key pair to parent that corresponds to child
+  */
+
+  void* parent = get_page(table->pager, parent_page_num);
+  void* child = get_page(table->pager, child_page_num);
+  uint32_t child_max_key = get_node_max_key(child);
+  uint32_t index = internal_node_find_child(parent, child_max_key);
+
+  uint32_t original_num_keys = *internal_node_num_keys(parent);
+  *internal_node_num_keys(parent) = original_num_keys + 1;
+
+  if (original_num_keys >= INTERNAL_NODE_MAX_CELLS) {
+    printf("Need to implement splitting internal node\n");
+    exit(EXIT_FAILURE);
+  } 
```

应插入新单元（子节点/键对）的索引取决于新子项中的最大键。在我们看到的例子中，`child_max_key`为 5，`index`为 1。

如果内部节点中没有其他单元的空间，则抛出错误。我们稍后会实现。

现在让我们看看函数的其余部分：

```diff
+
+  uint32_t right_child_page_num = *internal_node_right_child(parent);
+  void* right_child = get_page(table->pager, right_child_page_num);
+
+  if (child_max_key > get_node_max_key(right_child)) {
+    /* Replace right child */
+    *internal_node_child(parent, original_num_keys) = right_child_page_num;
+    *internal_node_key(parent, original_num_keys) =
+        get_node_max_key(right_child);
+    *internal_node_right_child(parent) = child_page_num;
+  } else {
+    /* Make room for the new cell */
+    for (uint32_t i = original_num_keys; i > index; i--) {
+      void* destination = internal_node_cell(parent, i);
+      void* source = internal_node_cell(parent, i - 1);
+      memcpy(destination, source, INTERNAL_NODE_CELL_SIZE);
+    }
+    *internal_node_child(parent, index) = child_page_num;
+    *internal_node_key(parent, index) = child_max_key;
+  }
+} 
```

因为我们将最右边的子指针与其他子/键对分开存储，所以如果新的子节点要成为最右边的子节点，我们必须以不同的方式处理事情。

在我们的例子中，我们将进入`else`块。首先，我们通过将其他单元向右移动一个空间来为新单元腾出空间（虽然在我们的例子中有 0 个单元要移动）。

接下来，我们将新的子指针和键写入由`index`确定的单元中。

为了减少所需的测试用例的大小，我现在正在硬编码`INTERNAL_NODE_MAX_CELLS`

```diff
@@ -126,6 +126,8 @@ 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;
+/* Keep this small for testing */
+const uint32_t INTERNAL_NODE_MAX_CELLS = 3; 
```

说到测试，我们的大型数据集测试通过我们的旧桩并进入我们的新桩：

```diff
@@ -65,7 +65,7 @@ describe 'database' do
     result = run_script(script)
     expect(result.last(2)).to match_array([
       "db > Executed.",
-      "db > Need to implement updating parent after split", +      "db > Need to implement splitting internal node",
     ]) 
```

非常满意，我知道。

我将添加另一个打印四节点树的测试。因此，我们测试的情况多于顺序 ID，此测试将以伪随机顺序添加记录。

```diff
+  it 'allows printing out the structure of a 4-leaf-node btree' do
+    script = [
+      "insert 18 user18 person18@example.com",
+      "insert 7 user7 person7@example.com",
+      "insert 10 user10 person10@example.com",
+      "insert 29 user29 person29@example.com",
+      "insert 23 user23 person23@example.com",
+      "insert 4 user4 person4@example.com",
+      "insert 14 user14 person14@example.com",
+      "insert 30 user30 person30@example.com",
+      "insert 15 user15 person15@example.com",
+      "insert 26 user26 person26@example.com",
+      "insert 22 user22 person22@example.com",
+      "insert 19 user19 person19@example.com",
+      "insert 2 user2 person2@example.com",
+      "insert 1 user1 person1@example.com",
+      "insert 21 user21 person21@example.com",
+      "insert 11 user11 person11@example.com",
+      "insert 6 user6 person6@example.com",
+      "insert 20 user20 person20@example.com",
+      "insert 5 user5 person5@example.com",
+      "insert 8 user8 person8@example.com",
+      "insert 9 user9 person9@example.com",
+      "insert 3 user3 person3@example.com",
+      "insert 12 user12 person12@example.com",
+      "insert 27 user27 person27@example.com",
+      "insert 17 user17 person17@example.com",
+      "insert 16 user16 person16@example.com",
+      "insert 13 user13 person13@example.com",
+      "insert 24 user24 person24@example.com",
+      "insert 25 user25 person25@example.com",
+      "insert 28 user28 person28@example.com",
+      ".btree",
+      ".exit",
+    ]
+    result = run_script(script) 
```

它将按原样输出：

```
- internal (size 3)
  - leaf (size 7)
    - 1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
  - key 1
  - leaf (size 8)
    - 8
    - 9
    - 10
    - 11
    - 12
    - 13
    - 14
    - 15
  - key 15
  - leaf (size 7)
    - 16
    - 17
    - 18
    - 19
    - 20
    - 21
    - 22
  - key 22
  - leaf (size 8)
    - 23
    - 24
    - 25
    - 26
    - 27
    - 28
    - 29
    - 30
db > 
```

仔细看，你会发现一个错误：

```
 - 5
    - 6
    - 7
  - key 1 
```

键应该是 7，而不是 1！

经过一堆调试，我发现这是由于一些错误的指针算法。

```diff
 uint32_t* internal_node_key(void* node, uint32_t key_num) {
-  return internal_node_cell(node, key_num) + INTERNAL_NODE_CHILD_SIZE; +  return (void*)internal_node_cell(node, key_num) + INTERNAL_NODE_CHILD_SIZE;
 } 
```

`INTERNAL_NODE_CHILD_SIZE`是 4。我的意图是在`internal_node_cell()`的结果中添加 4 个字节，但由于`internal_node_cell()`返回`uint32_t*`，这实际上是在添加`4 * sizeof(uint32_t)`字节。在进行算术运算之前，我通过转换为`void*`来修复它。

注意！[`void`指针的指针算法](https://stackoverflow.com/questions/3523145/pointer-arithmetic-for-void-pointer-in-c/46238658#46238658)不是 C 标准的一部分，可能无法与编译器兼容。我可能会在将来写一篇关于可移植性的文章，但我现在要留着我的无效指针算术。

好的。迈向功能全面的 b 树实现的又一步。下一步应该是拆分内部节点。下次再见！