B+Tree Indexes
Concept. A B+Tree index stores keys in sorted order in a balanced tree. It supports both point lookups and range scans (WHERE rating BETWEEN 4.0 AND 5.0) by scanning leaf pages in key order.
Intuition. Think of a dictionary: half a million words, but you never scan them all. You can jump to the right letter, the guide words at the top of each page narrow you to the right page. A B+Tree gives a database table that functionality. So when you query "find Songs between 'Lava' and 'Love'" it walks down the tree to ``Lava,'' then reads leaf pages in order until it passes ''Love.'' One descent, then a scan, not a tree walk per row.
Prerequisites. Index Fundamentals introduces (key, page#) pairs and pointers.
What a B+Tree Stores
A B+Tree indexes a table's data pages. Three layers of index nodes (root, internal, leaf) route a lookup down to the right page, and the leaves are linked left-to-right so a range scan can stream across them without going back to the root.
In this example, each song has a permanent catalog number (catalog_no), separate from its Spotify ID. (Imagine an industry-supplied number when a new song comes out.)
Figure: Build a hierarchy over sorted leavesB+Tree layout on Songs.catalog_no. Data pages on the left hold the rows. The tree on the right has a root node, three internal nodes, and a row of leaf nodes. Leaves store (key, data-page pointer) pairs in sorted order; neighbouring leaves are linked left-to-right so a range scan can walk across them without returning to the root. Color key: green is the index and data pages on disk (persistent), violet is the lookup trace for key 12890.
Figure 1. Data pages on disk store the Songs rows. The B+Tree on disk stores a root and internal nodes that route lookups to sorted leaves. Leaves store (key, data-page pointer) pairs and link left-to-right. For catalog_no = 12890 ("As It Was"), the root selects 5000..14999, the internal node selects 11234..13788, the leaf resolves 12890 → Page 3, and Page 3 contains the row. With no pages cached, the lookup reads three index pages and one data page: four IOs.
Walking a Range Query
The B+Tree finds the starting leaf, then streams across leaves until it passes the end key. The trace below walks 12000 ≤ catalog_no ≤ 14000.
Figure: Find catalog numbers: 12,000 ≤ key ≤ 14,000Walking a range query SELECT * WHERE catalog_no BETWEEN 12000 AND 14000 in a B+Tree. Step 1 visits the root. Step 2 descends to the middle internal node (the other two internals are skipped). Step 3 walks from leaf 3 to leaf 4 sequentially, loading data pages 3 and 4. Stop at 14001 in leaf 4. Total 6 IOs versus 160,000 for a full scan. Color key: green is the index and data pages on disk (persistent), violet arrows show the lookup path, grey dashed is skipped or not visited.
Figure 2. For 12000 ≤ catalog_no ≤ 14000, matching rows occupy data pages 3 and 4. The descent runs root → middle internal → Leaf 3, then follows leaf links. Leaf 3 yields 12890 and 13456. Leaf 4 yields 13789 and 13980. The next key in Leaf 4 is 14001, which exceeds 14000, so the scan stops without reading another leaf. The query reads 4 index pages and 2 data pages, 6 IOs total, versus roughly 160,000 for a full scan, about 27,000× fewer IOs.
The result is mostly sequential IO once the descent reaches the starting leaf, which makes range scans cheap on a B+Tree.
B+Trees: Few Reads, More Work on Writes
Fanout is the number of children per internal page. Higher fanout means fewer levels.
$$ f \approx \frac{\text{page size}}{\text{key size} + \text{pointer size}}, \qquad h \approx \left\lceil \log_f N \right\rceil $$
N = number of indexable keys; h = approximate number of index levels.
For 1 trillion keys and 8-byte pointers, ignoring headers and unused space:
| Page size | Key size | Approx. fanout | Index levels |
|---|---|---|---|
| 64 KB | 8 B | 4,096 | 4 |
| 64 KB | 1 KB | 63 | 7 |
| 64 MB | 8 B | 4.2 million | 2 |
| 64 MB | 1 KB | 65,027 | 3 |
-
Reads: High fanout keeps the tree shallow. Point queries follow one path; range queries continue through sorted leaves.
-
Writes: Inserts update pages. Full pages split, and splits can propagate upward. Maintaining balance adds work under heavy writes.
The height estimate assumes similar internal-node and leaf capacities and full pages; occupancy and page overhead affect the actual height.
Notes: Index levels are not the full lookup cost: fetching a data page adds IO, while cached index pages avoid disk reads. In practice, the root will be cached because all lookups start from that. We will not cover the balancing algorithms in cs145. They work similar to how splits work with Btree balancing algorithms (in RAM), except a bit trickier because we need to manage our I/Os carefully.
What B+Trees Handle Well, and What They Don't
Handles well
WHERE user_id = 42
WHERE rating BETWEEN 4.0 AND 5.0
WHERE name LIKE 'Ali%'
ORDER BY created_at
Equality, ranges, prefix matching, and sorted scans. Anything that benefits from keys being in order.