Skip to content

Commit cfa5078

Browse files
committed
Part 8 edits
1 parent eaaba31 commit cfa5078

File tree

3 files changed

+23
-33
lines changed

3 files changed

+23
-33
lines changed

_layouts/default.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ <h1>{{ site.title | default: site.github.repository_name }}</h1>
2121
<p class="view"><a href="{{ site.baseurl }}/">Overview</a></p>
2222

2323
{% if site.github.is_project_page %}
24-
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ github_name }}</small></a></p>
24+
<p class="view"><a href="{{ site.github.repository_url }}">View on GitHub (pull requests welcome)</a></p>
2525
{% endif %}
2626

2727
{% if site.github.is_user_page %}

_parts/part8.md

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
22
title: Part 8 - B-Tree Leaf Node Format
3-
date: 2017-09-24
3+
date: 2017-09-25
44
---
55

66
We're changing the format of our table from an unsorted array of rows to a B-Tree. This is a pretty big change that is going to take multiple articles to implement. By the end of this article, we'll define the layout of a leaf node and support inserting key/value pairs into a single-node tree. But first, let's recap the reasons for switching to a tree structure.
77

88
## Alternative Table Formats
99

10-
With the current format, each page stores only rows (no metadata) so it is pretty space efficient. Insertion is also fast because we just append it to the end. However, finding a particular row can only be done by scanning the entire table. And if we want to delete a row, we have to move every row that comes after it to fill in the hole.
10+
With the current format, each page stores only rows (no metadata) so it is pretty space efficient. Insertion is also fast because we just append to the end. However, finding a particular row can only be done by scanning the entire table. And if we want to delete a row, we have to to fill in the hole by movinvg every row that comes after it.
1111

1212
If we stored the table as an array, but kept rows sorted by id, we could use binary search to find a particular id. However, insertion would have the same problem as deletion where we have to move a lot of rows to make space.
1313

@@ -30,9 +30,9 @@ Leaf nodes and internal nodes have different layouts. Let's make an enum to keep
3030
+typedef enum NodeType_t NodeType;
3131
```
3232

33-
Each node will correspond to one page. Internal nodes will point to their children by storing the page number that stores the child. The btree receives pointers to pages by asking the pager for a particular page number.
33+
Each node will correspond to one page. Internal nodes will point to their children by storing the page number that stores the child. The btree asks the pager for a particular page number and gets back a pointer into the page cache. Pages are stored in the database file one after the other in order of page number.
3434

35-
Nodes need to store some metadata in a header at the beginning of the page. Both types of nodes will store what type of node they are, whether or not they are the root node, and a pointer to their parent (to allow finding a node's siblings). I define constants for the size and offset of every header field:
35+
Nodes need to store some metadata in a header at the beginning of the page. Every node will store what type of node it is, whether or not it is the root node, and a pointer to its parent (to allow finding a node's siblings). I define constants for the size and offset of every header field:
3636

3737
```diff
3838
+/*
@@ -79,13 +79,13 @@ The body of a leaf node is an array of cells. Each cell is a key followed by a v
7979
+ LEAF_NODE_SPACE_FOR_CELLS / LEAF_NODE_CELL_SIZE;
8080
```
8181

82-
Based on these constants, here's what the layout of leaf node looks like currently:
82+
Based on these constants, here's what the layout of a leaf node looks like currently:
8383

8484
{% include image.html url="assets/images/leaf-node-format.png" description="Our leaf node format" %}
8585

86-
It's a little space inefficient to use an entire byte per boolean value in the header, but this makes it a little easier to write code to access those values.
86+
It's a little space inefficient to use an entire byte per boolean value in the header, but this makes it easier to write code to access those values.
8787

88-
Also notice that there's some wasted space at the end. We store as many cells as we can after the header, but there is some space left over that can't hold an entire cell. We leave it empty to avoid splitting cells between nodes.
88+
Also notice that there's some wasted space at the end. We store as many cells as we can after the header, but the leftover space can't hold an entire cell. We leave it empty to avoid splitting cells between nodes.
8989

9090
## Accessing Leaf Node Fields
9191

@@ -96,7 +96,7 @@ The code to access keys, values and metadata all involve pointer arithmetic usin
9696
+ return node + LEAF_NODE_NUM_CELLS_OFFSET;
9797
+}
9898
+
99-
+uint32_t* leaf_node_cell(void* node, uint32_t cell_num) {
99+
+void* leaf_node_cell(void* node, uint32_t cell_num) {
100100
+ return node + LEAF_NODE_HEADER_SIZE + cell_num * LEAF_NODE_CELL_SIZE;
101101
+}
102102
+
@@ -167,7 +167,7 @@ Every node is going to take up exactly one page, even if it's not full. That mea
167167
printf("Error closing db file.\n");
168168
```
169169

170-
Now it makes more sense to store the number of pages in our database rather than the number of rows. The number of pages should be assoicated with the pager, object, not the table, since it's the number of pages used by the database, not a particular table.
170+
Now it makes more sense to store the number of pages in our database rather than the number of rows. The number of pages should be assoicated with the pager object, not the table, since it's the number of pages used by the database, not a particular table. A btree is identified by its root node page number, so the table object needs to keep track of that.
171171

172172
```diff
173173
const uint32_t PAGE_SIZE = 4096;
@@ -223,7 +223,7 @@ Now it makes more sense to store the number of pages in our database rather than
223223

224224
## Changes to the Cursor Object
225225

226-
A cursor represents a position in the table. When our table was a simple array of rows, that could be represented by a row number. Now that it's a tree, we identify a position by the page number of the node, and the cell number within that node.
226+
A cursor represents a position in the table. When our table was a simple array of rows, we could access a row given just the row number. Now that it's a tree, we identify a position by the page number of the node, and the cell number within that node.
227227

228228
```diff
229229
struct Cursor_t {
@@ -355,9 +355,9 @@ Next we'll make a function for inserting a key/value pair into a leaf node. It w
355355
+
356356
```
357357

358-
We holding off on implementing splitting for now, so we error if the node is full. Next we shift cells once space to the right to make room for the new cell. Then we write the new key/value into the empty space.
358+
We haven't implemented splitting yet, so we error if the node is full. Next we shift cells one space to the right to make room for the new cell. Then we write the new key/value into the empty space.
359359

360-
Since we're assuming the tree has only one node for now, our `execute_insert()` function simply needs to call this helper method:
360+
Since we assume the tree only has one node, our `execute_insert()` function simply needs to call this helper method:
361361

362362
```diff
363363
ExecuteResult execute_insert(Statement* statement, Table* table) {
@@ -386,7 +386,6 @@ How many rows can the leaf node hold?
386386
I'm adding a new meta command to print out a few constants of interest.
387387

388388
```diff
389-
+
390389
+void print_constants() {
391390
+ printf("ROW_SIZE: %d\n", ROW_SIZE);
392391
+ printf("COMMON_NODE_HEADER_SIZE: %d\n", COMMON_NODE_HEADER_SIZE);
@@ -440,8 +439,7 @@ To help with debugging and visualization, I'm also adding a meta command to prin
440439
```diff
441440
+void print_leaf_node(void* node) {
442441
+ uint32_t num_cells = *leaf_node_num_cells(node);
443-
+ printf("leaf (size %d)", num_cells);
444-
+ printf("\n");
442+
+ printf("leaf (size %d)\n", num_cells);
445443
+ for (uint32_t i = 0; i < num_cells; i++) {
446444
+ uint32_t key = *leaf_node_key(node, i);
447445
+ printf(" - %d : %d\n", i, key);
@@ -495,7 +493,7 @@ And a test
495493

496494
Uh oh, we're still not storing rows in sorted order. You'll notice that `execute_insert()` inserts into the leaf node at the position returned by `table_end()`. So rows are stored in the order they were inserted, just like before.
497495

498-
## Current Limitations
496+
## Next Time
499497

500498
This all might seem like a step backwards. Our database now stores fewer rows than it did before, and we're still storing rows in unsorted order. But like I said at the beginning, this is a big change and it's important to break it up into manageable steps.
501499

@@ -576,7 +574,7 @@ Next time, we'll implement finding a record by primary key, and start storing ro
576574
+ return node + LEAF_NODE_NUM_CELLS_OFFSET;
577575
+}
578576
+
579-
+uint32_t* leaf_node_cell(void* node, uint32_t cell_num) {
577+
+void* leaf_node_cell(void* node, uint32_t cell_num) {
580578
+ return node + LEAF_NODE_HEADER_SIZE + cell_num * LEAF_NODE_CELL_SIZE;
581579
+}
582580
+
@@ -599,8 +597,7 @@ Next time, we'll implement finding a record by primary key, and start storing ro
599597
+
600598
+void print_leaf_node(void* node) {
601599
+ uint32_t num_cells = *leaf_node_num_cells(node);
602-
+ printf("leaf (size %d)", num_cells);
603-
+ printf("\n");
600+
+ printf("leaf (size %d)\n", num_cells);
604601
+ for (uint32_t i = 0; i < num_cells; i++) {
605602
+ uint32_t key = *leaf_node_key(node, i);
606603
+ printf(" - %d : %d\n", i, key);
@@ -820,16 +817,10 @@ Next time, we'll implement finding a record by primary key, and start storing ro
820817
+ leaf_node_insert(cursor, row_to_insert->id, row_to_insert);
821818

822819
free(cursor);
823-
824-
diff --git a/spec/main_spec.rb b/spec/main_spec.rb
825-
index bc0180a..43c9043 100644
826-
--- a/spec/main_spec.rb
827-
+++ b/spec/main_spec.rb
828-
@@ -108,4 +108,44 @@ describe 'database' do
829-
"db > ",
830-
])
831-
end
832-
+
820+
```
821+
822+
And the specs:
823+
```diff
833824
+ it 'allows printing out the structure of a one-node btree' do
834825
+ script = [3, 1, 2].map do |i|
835826
+ "insert #{i} user#{i} person#{i}@example.com"

db.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ uint32_t* leaf_node_num_cells(void* node) {
128128
return node + LEAF_NODE_NUM_CELLS_OFFSET;
129129
}
130130

131-
uint32_t* leaf_node_cell(void* node, uint32_t cell_num) {
131+
void* leaf_node_cell(void* node, uint32_t cell_num) {
132132
return node + LEAF_NODE_HEADER_SIZE + cell_num * LEAF_NODE_CELL_SIZE;
133133
}
134134

@@ -151,8 +151,7 @@ void print_constants() {
151151

152152
void print_leaf_node(void* node) {
153153
uint32_t num_cells = *leaf_node_num_cells(node);
154-
printf("leaf (size %d)", num_cells);
155-
printf("\n");
154+
printf("leaf (size %d)\n", num_cells);
156155
for (uint32_t i = 0; i < num_cells; i++) {
157156
uint32_t key = *leaf_node_key(node, i);
158157
printf(" - %d : %d\n", i, key);

0 commit comments

Comments
 (0)