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.
- 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
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:
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:
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:
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.
| Bytes | Field | In an internal node |
|---|---|---|
| 0 | node type | 2 |
| 1 | is root | 1 for page 0, 0 otherwise |
| 2–5 | parent page | 0 for the root |
| 6–9 | count | the number of keys |
| 10–13 | link | the right child: the page holding every key from the last key up |
| from 14 | entries | 8 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:
- 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
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
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.Start at page 0 and read its node type.
- 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.Repeat until the node is a leaf. Search that leaf as in the previous stage.
- 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.
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.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.If the leaf has fewer than 8 cells, insert in order as before. Done.
- 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.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.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.Fix the links: the right leaf links to whatever the left leaf linked to, and the left leaf links to the right one.
- 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.
Walk the leaves
The cursor of the previous stage started at page 0. Now it starts at the leftmost leaf.
- 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.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.Invariant: the rows still come out in ascending id order, and SELECT itself did not change.
Print the tree
btree structure shows every node, from the root down.
- 1.Print the depth: 1 for a root leaf, one more for each level of internal nodes.
- 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.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.
| Input | Output |
|---|---|
| an INSERT that splits a leaf | Insert 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.
$droid --db users.dbWelcome 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 find 4[BTREE] Find key=4: FOUND (page=1 cell=3)>pager status[PAGER] Status: total_pages=3 cached=3>select id, name from users;id | name1 | ada2 | bob3 | cy4 | dan5 | eve6 | fay7 | gus8 | hal9 | ivy(9 rows)>insert into users (id, name, email) values (5, 'mallory', '[email protected]');[ERROR:00701] Key 5 already exists>.exitexiting.. 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 5reports 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 findreports 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 statusmust 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 × 9bytes, 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 splitfull-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 cellsfirst-split
btree dumpshows the internal node with its key, its child and its right child; the file holds three pages with the headers the layout gives themfirst-split- The ninth key may belong below, among or above the eightfirst-split-new-key-anywhere
btree findgoes through the root and reports the leaf's page and the key's rank in it; a key between the leaves is NOT_FOUNDfind-after-first-split- SELECT reads on from one leaf to the nextselect-across-two-leaves
- A duplicate key into a full leaf is 00701 and splits nothingduplicate-into-full-leaf
- Duplicates in either leaf, and of the separator key, are 00701 and change nothingduplicate-after-split
- A split tree is read back from the file after a restartsplit-survives-restart
- A full leaf read from the file splits like one built in the sessionsplit-in-a-later-session
- A full leaf under the root splits too, and
pager statuscounts one page per nodesecond-split - 20 rows inserted backwards or shuffled fit at depth 2, every leaf with at least 4 cellstwenty-rows-descending · twenty-rows-shuffled
- A tree grows across restarts, and the file holds a valid tree after each onegrow-across-restarts