droid
Leaf Splits and the First Internal Node
The B+Tree · Stage 10 of 16

Leaf Splits and the First Internal Node

When the leaf is full, split it in two and put a node above them

Split a full leaf into two leaves of 4 and 5 cells, give the tree its first internal node on page 0, route searches and inserts through it, chain the leaves so SELECT reads on from one to the next, and show the whole tree with btree structure.

Not run yet · 13 tests
Concepts in this stage
  • A split: a full leaf takes the new cell anyway and divides into two leaves, the lower keys on the left
  • The separator: a copy of the right leaf's first key, stored in the parent to tell searches which way to go
  • The internal node: keys and child pages, never rows; its own page layout after the same 14-byte header
  • The root stays on page 0: when the root leaf splits, its cells move out, and page 0 becomes the internal node
  • The leaf chain: each leaf's link names the next leaf, so a scan never climbs back up the tree
  • Routing: a key equal to a separator lives on the separator's right
Where this stage sits in the engine
Talking to the user
REPL
Sessions
Printer
Server
Wire protocol
Understanding SQL
LexerParser
Validator
Answering a query
Planner
Executor
Keeping changes safe
Transactions
Write-ahead log
Undo log
Lock manager
Read views
Storing rows
Row format
Slotted page
B+Tree
Reaching the disk
PagerDatabase file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

Why this stage exists

At the end of the previous stage the table is one sorted leaf, and the ninth row is refused with 00702. A table that stops at eight rows is not a table. This stage removes the limit. A full leaf splits in two, and a new kind of node, the internal node, sits above the two leaves and tells a search which one to read.

That is the whole idea of a B+Tree. Rows live only in leaves, and leaves stay a fixed size. When one fills up it becomes two half-full leaves, and the node above gains a key. Nothing is ever shifted across the whole table, and a search reads one node per level, not every row.

The split happens in the leaves, and in this stage the root internal node gets up to 3 keys, which is 4 leaves. What happens when the internal node is full is the next stage.

What changes: one leaf becomes three nodes

Insert rows 1 to 8 and the root leaf is full. The ninth row arrives:

Before: the root leaf is full, and key 9 arrives
leaf · page 0 · full
[1,2,3,4,5,6,7,8]
next → none
After the split
internal · page 0 · changed
[5]
leaf · page 1 · new
[1,2,3,4]
next → page 2
leaf · page 2 · new
[5,6,7,8,9]
next → none
The nine keys divide 4 and 5. Page 0 is still the root, now an internal node whose one key, 5, is a copy of the first key of the right leaf. The rows moved to two new pages, 1 and 2, and page 1 links to page 2.

Three things to notice.

The root stays on page 0. Every search starts at page 0, so the root can never move. When the root leaf splits, it is its cells that move out, into two new pages, and page 0 is rewritten as an internal node. The file grows from one page to three.

The separator is a copy. Key 5 is in two places: in the right leaf with its row, as a cell like any other, and in the root as a bare 4-byte number. The root holds no rows. It only says: keys below 5 are in page 1; 5 and above are in page 2.

The halves are 4 and 5, either way round. Nine cells do not divide evenly. You may keep 4 on the left and move 5 right, as above, or keep 5 and move 4, which puts 6 in the root. Both are correct and the tests accept both. Pick one and always use it. What is not allowed is anything more lopsided: a split that leaves 8 and 1 makes a leaf that is full again at the next insert, and the tests count on every leaf holding at least 4 cells.

The split, cell by cell

The same split seen as cells. The new key takes its place among the eight first, so the nine keys are in order before they are divided:

The eight cells of page 0 and key 9, in key order, held outside the page while they are divided
header
cells = 9
cell 0
key = 1
cell 1
key = 2
cell 2
key = 3
cell 3
key = 4
cell 4
key = 5
will move
cell 5
key = 6
will move
cell 6
key = 7
will move
cell 7
key = 8
will move
cell 8
key = 9
new
free space
Page 1, the left leaf
header
cells = 4
cell 0
key = 1
moved
cell 1
key = 2
moved
cell 2
key = 3
moved
cell 3
key = 4
moved
free space
Page 2, the right leaf
header
cells = 5
cell 0
key = 5
moved
cell 1
key = 6
moved
cell 2
key = 7
moved
cell 3
key = 8
moved
cell 4
key = 9
moved
free space
Each cell is 64 bytes. A cell that moves one position moves by 64 bytes.
Nine cells never have to fit in one page: hold them somewhere of your own while you divide them. When the root splits, both halves go to new pages; when any other leaf splits, the left half stays where it was and only the right half moves.

A split below the root

Keep inserting and the right leaf fills: rows 5 to 12 are eight cells. Row 13 splits it, and this time the leaf is not the root:

Before: page 2 is full, and key 13 arrives
internal · page 0
[5]
leaf · page 1
[1,2,3,4]
next → page 2
leaf · page 2 · full
[5,6,7,8,9,10,11,12]
next → none
After: page 2 keeps the left half, page 3 is new, and the root gains key 9
internal · page 0 · changed
[5,9]
leaf · page 1
[1,2,3,4]
next → page 2
leaf · page 2 · changed
[5,6,7,8]
next → page 3
leaf · page 3 · new
[9,10,11,12,13]
next → none
Only one page is allocated. The full leaf keeps its page and its lower half; the upper half goes to the new page 3. Page 2 used to be the last leaf, so page 3 now is: it takes over page 2's link of 0, and page 2 links to 3. The root gains the separator 9, with page 3 as the child just after page 2.

The parent needs the new key and the new child at the right position: just after the child that split. In a node with keys [5], a split of the right child puts 9 after 5. A split of the left child would put its separator before 5, and every key and child after it moves along, as cells did in a sorted leaf.

A split never allocates anything else. After it, the pages in the file are exactly the nodes of the tree, which pager status and btree structure both show.

The internal node

An internal node is a page with the same 14-byte header as a leaf, and then entries of 8 bytes: the page of a child, then a key. A node with n keys has n entries and n + 1 children. The last child has no key to go with it, so it goes in the header's link field, which in an internal node is called the right child.

BytesFieldIn an internal node
0node type2
1is root1 for page 0, 0 otherwise
2–5parent page0 for the root
6–9countthe number of keys
10–13linkthe right child: the page holding every key from the last key up
from 14entries8 bytes each: the child's page, then the key. The child holds the keys below that key, and at or above the key before it

Page 0 after the split above, with the one entry that points at page 1 and the right child, page 2:

0
02
01
00
00
00
00
01
00
00
00
02
00
00
00
01
00
00
00
05
00
00
00
  • typebytes 0–0 (1 bytes) · 2: internal
  • is rootbytes 1–1 (1 bytes) · 1: this is page 0
  • parentbytes 2–5 (4 bytes) · 0: the root has none
  • countbytes 6–9 (4 bytes) · 1 key
  • right childbytes 10–13 (4 bytes) · page 2: keys 5 and above
  • childbytes 14–17 (4 bytes) · entry 0: page 1
  • keybytes 18–21 (4 bytes) · entry 0: key 5; page 1 holds the keys below it
An internal node's header, then one 8-byte entry. A second key would add its entry at byte 22. What comes after the last entry is not part of the node, whatever it holds: here it is what is left of the cells page 0 held as a leaf, if you did not clear it.

The two leaves have headers too, and a split must keep them true: node type 1; is root 0, since only page 0 is the root; parent 0, because page 0 is their parent; the count; and the link. Page 1 links to page 2, and page 2 links to 0, the end of the chain. Page 0 can never be a next leaf, so 0 is free to mean "none". The tests read all of this from the file.

How much fits at depth 2

The root can hold 3 keys in this stage, so the tree stops at 4 leaves. The split that would need a fifth leaf needs a fourth key in a full root, and that is still 00702 until the next stage splits internal nodes too.

How many rows get there depends on the order they arrive and on which way your splits go, but never fewer than 20. When a fifth leaf is needed there are already four leaves: the full one with 8 cells and three others that each kept at least 4 from their split. That is 20 rows, and the one arriving is the 21st. The tests never insert more than 20 rows in this stage.

The algorithm in words

1

Find the leaf for a key

Every search, every INSERT and btree find now start with this descent. On a tree that is one leaf it ends at once, at page 0.

  1. 1.Start at page 0 and read its node type.
  2. 2.At an internal node, go to the child of the first key that is greater than the key you want. If no key is greater, go to the right child.
  3. 3.Repeat until the node is a leaf. Search that leaf as in the previous stage.
  4. 4.Invariant: a key equal to a separator is always found on the right of it, because the separator is a copy of the first key of its right subtree.
2

Insert, and split a leaf that is full

The previous stage refused a ninth cell with 00702. Now the leaf makes room by becoming two.

  1. 1.Find the leaf. If the key is already in it, stop: 00701, and nothing changes. This check comes before anything else, even in a full leaf.
  2. 2.If the leaf has fewer than 8 cells, insert in order as before. Done.
  3. 3.Otherwise put the nine cells in key order, the new one among them, and divide them: the lower 4 or 5 on the left, the rest on the right. Choose one of the two ways and always use it.
  4. 4.If the leaf is not the root: keep the left half in its page, write the right half to one newly allocated page, and give the parent the right leaf's first key as a new separator, with the new page as the child just after the old one.
  5. 5.If the leaf is the root: allocate two new pages, the left half to the first and the right half to the second. Then rewrite page 0 as an internal node with one key, the right leaf's first key, pointing at the left page in its entry and at the right page as its right child.
  6. 6.Fix the links: the right leaf links to whatever the left leaf linked to, and the left leaf links to the right one.
  7. 7.Invariants: each half has 4 or 5 cells; every key is in exactly one leaf; each separator equals the first key of the leaf to its right; a split allocates only the pages it writes.
3

Walk the leaves

The cursor of the previous stage started at page 0. Now it starts at the leftmost leaf.

  1. 1.Start: descend from page 0, always to the first child, until you reach a leaf. On an empty table the cursor starts at the end.
  2. 2.Advance: move to the next cell. At the end of a leaf, follow its link to the next leaf and continue there. A link of 0 means this was the last leaf.
  3. 3.Invariant: the rows still come out in ascending id order, and SELECT itself did not change.
4

Print the tree

btree structure shows every node, from the root down.

  1. 1.Print the depth: 1 for a root leaf, one more for each level of internal nodes.
  2. 2.Visit the nodes depth first, children from left to right, the root first. Print one line per node, indented two more spaces than its parent.
  3. 3.An internal node prints its page and keys, a leaf its page, its number of cells and its keys.

Output contract for this stage

These are the exact lines the tests check. The full definition is on the output contract page, sections 7, 9 and 10, including the page layouts above.

InputOutput
an INSERT that splits a leafInsert statement executed successfully, as for any INSERT; the split prints nothing of its own
btree structure[BTREE] Tree depth: <d>, then one line per node, depth first, children left to right, two more spaces of indent per level: [BTREE] INTERNAL (page=<p> keys=[k1,k2]) and [BTREE] LEAF (page=<p> cells=<n> keys=[k1,k2,...])
btree dump <page>, an internal node[BTREE] Page <p>: type=INTERNAL num_keys=<n> right_child=<page>, then per key [BTREE] Key <i>: key=<k> (child_page=<page>)
btree find <key>, key present[BTREE] Find key=<k>: FOUND (page=<p> cell=<i>), where <p> is the leaf holding the key and <i> its rank in that leaf, then the Row: line
pager status after the first split[PAGER] Status: total_pages=3 cached=3
INSERT of a key the tree has, into any leaf, full or not[ERROR:00701] <message>, and nothing splits

Key lists have no spaces: keys=[1,2,3,4]. A tree that is only its root leaf has depth 1 and prints one line, [BTREE] LEAF (page=0 cells=<n> keys=[...]). btree dump with no page still dumps page 0, which is now the internal node.

What you should see when done

This session splits 4 and 5. If yours splits 5 and 4, the root holds 6, page 1 holds 1 to 5, and key 5 is found in page 1, cell 4.

shell
$droid --db users.db
Welcome to droid!
>insert into users (id, name, email) values (5, 'eve', '[email protected]');
Insert statement executed successfully
>insert into users (id, name, email) values (8, 'hal', '[email protected]');
Insert statement executed successfully
>insert into users (id, name, email) values (1, 'ada', '[email protected]');
Insert statement executed successfully
>insert into users (id, name, email) values (7, 'gus', '[email protected]');
Insert statement executed successfully
>insert into users (id, name, email) values (2, 'bob', '[email protected]');
Insert statement executed successfully
>insert into users (id, name, email) values (6, 'fay', '[email protected]');
Insert statement executed successfully
>insert into users (id, name, email) values (4, 'dan', '[email protected]');
Insert statement executed successfully
>insert into users (id, name, email) values (3, 'cy', '[email protected]');
Insert statement executed successfully
>btree structure
[BTREE] Tree depth: 1
[BTREE] LEAF (page=0 cells=8 keys=[1,2,3,4,5,6,7,8])
>insert into users (id, name, email) values (9, 'ivy', '[email protected]');
Insert statement executed successfully
>btree structure
[BTREE] Tree depth: 2
[BTREE] INTERNAL (page=0 keys=[5])
[BTREE] LEAF (page=1 cells=4 keys=[1,2,3,4])
[BTREE] LEAF (page=2 cells=5 keys=[5,6,7,8,9])
>btree dump 0
[BTREE] Page 0: type=INTERNAL num_keys=1 right_child=2
[BTREE] Key 0: key=5 (child_page=1)
>btree find 5
[BTREE] Find key=5: FOUND (page=2 cell=0)
[BTREE] Row: id=5 name='eve' email='[email protected]'
>btree find 4
[BTREE] Find key=4: FOUND (page=1 cell=3)
[BTREE] Row: id=4 name='dan' email='[email protected]'
>pager status
[PAGER] Status: total_pages=3 cached=3
>select id, name from users;
id | name
1 | ada
2 | bob
3 | cy
4 | dan
5 | eve
6 | fay
7 | gus
8 | hal
9 | ivy
(9 rows)
>insert into users (id, name, email) values (5, 'mallory', '[email protected]');
[ERROR:00701] Key 5 already exists
>.exit
exiting.. good bye!

Key 5, the separator, is found on the right, in page 2 cell 0. SELECT did not change at all, and still prints the rows in order: the cursor now starts at page 1 and follows the link to page 2.

Common bugs in this stage

  • The root moves. The split writes the new internal node to a fresh page and leaves page 0 as a leaf, or makes the old page 0 the left leaf. The next search starts at page 0 and finds only half the table. The root is page 0, always.
  • Splitting before the duplicate check. A duplicate arriving at a full leaf splits it and then reports 00701. The statement failed, and the tree changed anyway. Look for the key first.
  • Writing the ninth cell past the end of the page. Eight cells and a header fill most of a page, but nine may not be written there even for a moment. Collect the nine cells outside the page, then write each half.
  • The separator from the wrong side. The separator is the first key of the right leaf. The last key of the left leaf works for searches until someone inserts a key between the two, and then that key goes to the wrong leaf.
  • Going left on equal. A search for key 5 with separator 5 must go right. With "less than or equal goes left", btree find 5 reports NOT_FOUND for a key that is there.
  • Forgetting the link. The left leaf still links to 0 after the split, so SELECT stops after the first leaf and returns four rows. Or the right leaf links to 0 when the left one had a next leaf, and a split in the middle of the chain cuts it in two.
  • Stale headers. A new leaf copied from page 0 keeps is root 1. Count, is root, parent and link are all yours to set on every page a split writes.
  • The new separator in the wrong place. Appending the new key to the end of the parent's keys works when the rightmost leaf split, which is every split when rows arrive in order. Insert rows backwards and it breaks. The key and its child go just after the child that split.
  • btree find reports the wrong page. It reports page 0, or the cell as if all the rows were in one leaf. The page is the leaf's, and the cell counts from 0 in that leaf.
  • An extra page. Allocating a page "just in case", or three pages for a root split, leaves a page the tree does not use. pager status must count only the tree's pages.

Language notes

The page above is the same for every language. These are the only language-specific hints.

  • C and C++: a buffer of nine cells on the stack, 64 × 9 bytes, is enough to hold a leaf while it splits. Give the internal node its own read and write functions, rather than reusing the leaf's with different offsets.
  • Rust: a split touches two or three pages at once. Borrowing them all mutably from the pager at the same time fights the borrow checker. Copy the cells out, drop the borrow, then write each page.
  • Zig: keep the node header in one place and read it the same way for both kinds of node, since only what follows it differs.

Checklist

One line per thing the tests check. The tags name the test cases.

  • A leaf of 8 cells is full, and does not split
    full-leaf-does-not-split
  • The ninth row splits the root leaf: page 0 becomes internal with one key, over leaves on pages 1 and 2 of 4 and 5 cells
    first-split
  • btree dump shows the internal node with its key, its child and its right child; the file holds three pages with the headers the layout gives them
    first-split
  • The ninth key may belong below, among or above the eight
    first-split-new-key-anywhere
  • btree find goes through the root and reports the leaf's page and the key's rank in it; a key between the leaves is NOT_FOUND
    find-after-first-split
  • SELECT reads on from one leaf to the next
    select-across-two-leaves
  • A duplicate key into a full leaf is 00701 and splits nothing
    duplicate-into-full-leaf
  • Duplicates in either leaf, and of the separator key, are 00701 and change nothing
    duplicate-after-split
  • A split tree is read back from the file after a restart
    split-survives-restart
  • A full leaf read from the file splits like one built in the session
    split-in-a-later-session
  • A full leaf under the root splits too, and pager status counts one page per node
    second-split
  • 20 rows inserted backwards or shuffled fit at depth 2, every leaf with at least 4 cells
    twenty-rows-descending · twenty-rows-shuffled
  • A tree grows across restarts, and the file holds a valid tree after each one
    grow-across-restarts