# 第 12 部分 - 扫描多级 B 树

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

我们现在支持构建一个多级 B 树，但是我们在这个过程中破坏了`select`语句。这是一个插入 15 行然后尝试打印它们的测试用例。

```diff
+  it 'prints all rows in a multi-level tree' do
+    script = []
+    (1..15).each do |i|
+      script << "insert #{i} user#{i} person#{i}@example.com"
+    end
+    script << "select"
+    script << ".exit"
+    result = run_script(script)
+
+    expect(result[15...result.length]).to match_array([
+      "db > (1, user1, person1@example.com)",
+      "(2, user2, person2@example.com)",
+      "(3, user3, person3@example.com)",
+      "(4, user4, person4@example.com)",
+      "(5, user5, person5@example.com)",
+      "(6, user6, person6@example.com)",
+      "(7, user7, person7@example.com)",
+      "(8, user8, person8@example.com)",
+      "(9, user9, person9@example.com)",
+      "(10, user10, person10@example.com)",
+      "(11, user11, person11@example.com)",
+      "(12, user12, person12@example.com)",
+      "(13, user13, person13@example.com)",
+      "(14, user14, person14@example.com)",
+      "(15, user15, person15@example.com)",
+      "Executed.", "db > ",
+    ])
+  end 
```

但是，当我们现在运行该测试用例时，实际发生的是：

```
db > select
(2, user1, person1@example.com)
Executed. 
```

那真是怪了。它只打印一行，该行看起来已损坏（注意 id 与用户名不匹配）。

奇怪的是因为`execute_select()`从表的开头开始，我们当前的`table_start()`实现返回根节点的单元 0。但是我们树的根节点现在是一个不包含任何行的内部节点。打印的数据必须在根节点为叶子时保留。`execute_select()`应该真正返回最左边叶子节点的单元 0。

所以去掉旧的实现：

```diff
-Cursor* table_start(Table* table) {
-  Cursor* cursor = malloc(sizeof(Cursor));
-  cursor->table = table;
-  cursor->page_num = table->root_page_num;
-  cursor->cell_num = 0;
-
-  void* root_node = get_page(table->pager, table->root_page_num);
-  uint32_t num_cells = *leaf_node_num_cells(root_node);
-  cursor->end_of_table = (num_cells == 0);
-
-  return cursor;
-} 
```

并添加一个搜索键 0（最小可能键）的新实现。即使表中不存在键 0，此方法也将返回最低 id 的位置（最左侧叶子节点的开始）。

```diff
+Cursor* table_start(Table* table) {
+  Cursor* cursor =  table_find(table, 0);
+
+  void* node = get_page(table->pager, cursor->page_num);
+  uint32_t num_cells = *leaf_node_num_cells(node);
+  cursor->end_of_table = (num_cells == 0);
+
+  return cursor;
+} 
```

通过这些更改，它仍然只打印出一个节点的行数：

```
db > select
(1, user1, person1@example.com)
(2, user2, person2@example.com)
(3, user3, person3@example.com)
(4, user4, person4@example.com)
(5, user5, person5@example.com)
(6, user6, person6@example.com)
(7, user7, person7@example.com)
Executed.
db > 
```

有 15 个条目，我们的 b 树包含一个内部节点和两个叶子节点，如下所示：

![structure of our btree](img/9b1d283f73bd4b427dee7e63ddf2f106.jpg)

我们的树的结构

要扫描整个表，我们需要在到达第一个叶子节点后跳转到第二个叶子节点。为此，我们将在名为`next_leaf`的叶子节点标头中保存一个新字段，该字段将保存右侧叶子的兄弟节点的页码。最右边的叶子节点的`next_leaf`值为 0 表示没有兄弟（无论如何，页面 0 都保留给表的根节点）。

更新叶子节点标头格式来包含新字段：

```diff
 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; +const uint32_t LEAF_NODE_NEXT_LEAF_SIZE = sizeof(uint32_t);
+const uint32_t LEAF_NODE_NEXT_LEAF_OFFSET =
+    LEAF_NODE_NUM_CELLS_OFFSET + LEAF_NODE_NUM_CELLS_SIZE;
+const uint32_t LEAF_NODE_HEADER_SIZE = COMMON_NODE_HEADER_SIZE +
+                                       LEAF_NODE_NUM_CELLS_SIZE +
+                                       LEAF_NODE_NEXT_LEAF_SIZE; 
```

添加一个方法来访问新字段：

```diff
+uint32_t* leaf_node_next_leaf(void* node) {
+  return node + LEAF_NODE_NEXT_LEAF_OFFSET;
+} 
```

初始化新叶子节点时，默认情况下将`next_leaf`设置为 0：

```diff
@@ -322,6 +330,7 @@ void initialize_leaf_node(void* node) {
   set_node_type(node, NODE_LEAF);
   set_node_root(node, false);
   *leaf_node_num_cells(node) = 0;
+  *leaf_node_next_leaf(node) = 0;  // 0 represents no sibling
 } 
```

每当我们拆分叶子节点时，更新兄弟指针。旧的叶子的兄弟成了新的叶子，新叶子的兄弟成了旧的叶子的兄弟。

```diff
@@ -659,6 +671,8 @@ void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) {
   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);
+  *leaf_node_next_leaf(new_node) = *leaf_node_next_leaf(old_node);
+  *leaf_node_next_leaf(old_node) = new_page_num; 
```

添加新字段会更改一些常量：

```diff
 it 'prints constants' do
     script = [
       ".constants",
@@ -199,9 +228,9 @@ describe 'database' do
       "db > Constants:",
       "ROW_SIZE: 293",
       "COMMON_NODE_HEADER_SIZE: 6",
-      "LEAF_NODE_HEADER_SIZE: 10", +      "LEAF_NODE_HEADER_SIZE: 14",
       "LEAF_NODE_CELL_SIZE: 297",
-      "LEAF_NODE_SPACE_FOR_CELLS: 4086", +      "LEAF_NODE_SPACE_FOR_CELLS: 4082",
       "LEAF_NODE_MAX_CELLS: 13",
       "db > ",
     ]) 
```

现在，只要我们想要将游标前进到叶子节点的末尾，我们就可以检查叶子节点是否有兄弟节点。如果是的话，跳转到它。否则，我们就在表的末尾。

```diff
@@ -428,7 +432,15 @@ void cursor_advance(Cursor* cursor) {

   cursor->cell_num += 1;
   if (cursor->cell_num >= (*leaf_node_num_cells(node))) {
-    cursor->end_of_table = true; +    /* Advance to next leaf node */
+    uint32_t next_page_num = *leaf_node_next_leaf(node);
+    if (next_page_num == 0) {
+      /* This was rightmost leaf */
+      cursor->end_of_table = true;
+    } else {
+      cursor->page_num = next_page_num;
+      cursor->cell_num = 0;
+    }
   }
 } 
```

在这些变化之后，我们实际打印了 15 行......

```
db > select
(1, user1, person1@example.com)
(2, user2, person2@example.com)
(3, user3, person3@example.com)
(4, user4, person4@example.com)
(5, user5, person5@example.com)
(6, user6, person6@example.com)
(7, user7, person7@example.com)
(8, user8, person8@example.com)
(9, user9, person9@example.com)
(10, user10, person10@example.com)
(11, user11, person11@example.com)
(12, user12, person12@example.com)
(13, user13, person13@example.com)
(1919251317, 14, on14@example.com)
(15, user15, person15@example.com)
Executed.
db > 
```

......但其中一个看起来已经损坏了

```
(1919251317, 14, on14@example.com) 
```

经过一些调试后，我发现它是因为我们分割叶子节点的错误：

```diff
@@ -676,7 +690,9 @@ void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) {
     void* destination = leaf_node_cell(destination_node, index_within_node);

     if (i == cursor->cell_num) {
-      serialize_row(value, destination); +      serialize_row(value,
+                    leaf_node_value(destination_node, index_within_node));
+      *leaf_node_key(destination_node, index_within_node) = key;
     } else if (i > cursor->cell_num) {
       memcpy(destination, leaf_node_cell(old_node, i - 1), LEAF_NODE_CELL_SIZE);
     } else { 
```

请记住，叶子节点中的每个单元首先包含一个键，然后是一个值：

![Original leaf node format](img/589aa888133cca0e6e724edc7ccf15c7.jpg)

原始叶子节点格式

我们将新行（值）写入单元的开头，键应该放在该单元的开头。这意味着用户名的一部分进入 id 的部分（因此 id 出奇的大）。

修复该 bug 后，我们最终按预期打印出整个表：

```
db > select
(1, user1, person1@example.com)
(2, user2, person2@example.com)
(3, user3, person3@example.com)
(4, user4, person4@example.com)
(5, user5, person5@example.com)
(6, user6, person6@example.com)
(7, user7, person7@example.com)
(8, user8, person8@example.com)
(9, user9, person9@example.com)
(10, user10, person10@example.com)
(11, user11, person11@example.com)
(12, user12, person12@example.com)
(13, user13, person13@example.com)
(14, user14, person14@example.com)
(15, user15, person15@example.com)
Executed.
db > 
```

哦！一个接一个的错误，但我们正在取得进展。

下次再见。