1. Introduction & Motivation
A balanced binary search tree such as an AVL or Red-Black tree keeps its height at $O(\log_2 n)$, and a lookup or insert touches roughly that many nodes. On a machine where every node lives in RAM next to every other node, that is an excellent bound. But indexes for databases and filesystems do not live purely in RAM — they live on disk, or are backed by disk and only partially cached, and each node visited during a search is a candidate disk seek or page fetch. A tree with height 30 can mean up to 30 seeks per lookup, and a single disk seek is roughly five to six orders of magnitude slower than a comparison already sitting in a CPU register.
The B-Tree fixes this by trading depth for breadth. Instead of two children per node, a B-Tree node holds many sorted keys and fans out into many children at once — its branching factor, called the order $m$ (or parameterized by a minimum degree $t$, where $m = 2t$), is chosen so that one node fills exactly one disk page, often hundreds of keys wide. With branching factor $m$, the height of a B-Tree over $n$ keys is $O(\log_m n)$ instead of $O(\log_2 n)$. For $n = 10^9$ and $m = 200$, a binary tree needs roughly 30 levels; a B-Tree needs about 4. Four disk seeks instead of thirty is the entire reason every production relational database index, and most filesystem directory structures, are built on B-Trees or their close cousin the B+ Tree, and not on binary search trees.
A B-Tree is best understood as the disk-aware, multiway generalization of the balanced binary search tree you already know: same ordering discipline, same $\log n$ height guarantee, but rebalanced by splitting and merging whole nodes instead of by single rotations, and tuned so that “one node” means “one page transfer” rather than “one pointer chase.”
2. Prerequisites
This lesson assumes you are comfortable with general binary search trees and the idea of keeping one height-balanced through rotations or recoloring, covered in Part 10: BST, AVL & Red-Black Trees. A B-Tree keeps the same ordering discipline a BST does — find a value by comparing it against keys stored in a node and descending into the child whose range must contain it — but it generalizes the balance invariant from “at most two children, rebalanced by rotation” to “between $t$ and $2t$ children, rebalanced by splitting and merging nodes.” If self-balancing search trees are still unfamiliar, read that lesson first; everything below assumes its vocabulary of keys, ordering invariants, height, and $\log n$ operations.
Additional vocabulary specific to this lesson:
- Minimum degree $t$: every node except the root must hold at least $t-1$ keys and at most $2t-1$ keys; an internal node with $k$ keys has exactly $k+1$ children.
- Order $m$: an equivalent parameterization, $m = 2t$, describing the maximum number of children a node may have.
- Full node: a node already holding the maximum $2t-1$ keys; it must be split before another key can be inserted into it.
- Leaf depth uniformity: every leaf in a B-Tree sits at exactly the same depth, unlike a general BST where leaves can be at different depths.
3. A Concrete Example
Inserting into a B-Tree of minimum degree t = 2
With $t=2$, every node may hold at most $2t-1=3$ keys and at least $t-1=1$ key (except the root). Insert the keys 10, 20, 5, 6, 12, 30, 7, 17 one at a time, always splitting a full node on the way down rather than after the insert fails.
| Insert | What happens | Root after |
|---|---|---|
| 10, 20 | Both fit in the empty root leaf. | [10, 20] |
| 5 | Still fits; the root leaf now holds the maximum of 3 keys. | [5, 10, 20] |
| 6 | The root is full (3 keys), so before descending it is split: the median key 10 moves up into a brand-new root, leaving two children [5] and [20]. Then 6 is inserted into the left child. | [10] over [5, 6] and [20] |
| 12, 30, 7 | Each descends into a non-full leaf and is inserted directly. | [10] over [5, 6, 7], [12, 20, 30] |
| 17 | The target leaf [12, 20, 30] is full, so it is split first: median 20 moves up beside 10 in the root, leaving children [12] and [30]. Then 17 is inserted into [12]. | [10, 20] over [5, 6, 7], [12, 17], [30] |
The final tree has a root [10, 20] with three leaf children [5, 6, 7], [12, 17], and [30]. Every leaf sits at depth 1, and two splits happened along the way — one that grew the tree’s height by creating a new root, and one that stayed at the existing height. Section 6 below reproduces this exact trace in runnable code.
4. Core Insight
Contrast this with a naive approach: insert the key first, then check whether the node overflowed and, if so, split it and propagate the split upward. That works, but it requires either a second upward pass or storing a full recursive call stack of “parent” nodes to revisit. Splitting proactively — check whether the child about to be descended into is full, and if so split it before descending — means a single top-down pass suffices, and the recursive insert code never needs to look back upward.
5. B-Tree Search
Search starts at the root and, in each node, scans the sorted keys to find the first key not smaller than the target. If that key matches, the search is done. Otherwise the search descends into the child that must contain the target’s range. A node with $k$ keys costs $O(k)$ per level (or $O(\log k)$ with binary search inside the node), and the tree has $O(\log_m n)$ levels.
6. B-Tree Insert (With Node Splitting)
Insert descends from the root exactly like search, but first checks whether the root itself is full, and while descending checks whether the child about to be entered is full — splitting it first if so. This reproduces the worked trace from Section 3.
7. B-Tree Delete
Delete is the trickiest B-Tree operation because removing a key must never leave a non-root node with fewer than $t-1$ keys. Before descending into a child, the algorithm guarantees that child has at least $t$ keys, by borrowing one key from a sibling that can spare it, or by merging with a sibling when neither can. Three cases in total:
- Key is in a leaf: remove it directly — the leaf was already guaranteed to have at least $t$ keys before descending, so it stays legal.
- Key is in an internal node: replace it with its predecessor (largest key in the left subtree) or successor (smallest key in the right subtree) if that child has at least $t$ keys, then recursively delete the replacement from that child; otherwise merge the two children around the key and recurse into the merged node.
- Key is not in this node: before descending into the child that must contain it, if that child has only $t-1$ keys, either borrow a key from an adjacent sibling (rotating a key through the parent) or merge it with a sibling (pulling a key down from the parent).
8. B+ Tree: What’s Different
That single structural change has an outsized payoff for range queries. In a plain B-Tree, keys can live at internal nodes, so a range scan such as SQL’s WHERE age BETWEEN 20 AND 30 must repeatedly re-descend the tree (or do an in-order traversal juggling a stack) to visit keys scattered across internal and leaf nodes in sorted order. In a B+ Tree, every key lives in a leaf, and leaves are already threaded together in sorted order, so a range scan is: search once for the starting key to find the first leaf, then walk the next pointers, reading off keys until the upper bound is passed. No re-descent, no stack — just a linked-list walk. That is precisely why B+ Trees, not plain B-Trees, are the standard structure behind relational database indexes: point lookups are $O(\log_m n)$ exactly like a B-Tree, and range scans are $O(\log_m n)$ to find the start plus $O(k)$ to stream $k$ matching rows, with no extra tree traversal cost per row.
9. B+ Tree Search and Range Scan
Descending to a leaf works like a B-Tree search, except the comparison rule only needs to pick the correct child — the actual key match, if any, is confirmed once the leaf is reached. A range scan finds the starting leaf the same way, then walks next pointers, collecting keys until one exceeds the upper bound.
10. Complexity Analysis
The Big-O class, $O(\log n)$, is the same family as a balanced BST’s. What changes is the constant hidden inside $\log_m$ versus $\log_2$, and that constant is the entire point in a disk-backed setting: minimizing tree height, not just achieving a logarithmic order, is what matters, because height is measured in disk seeks, and a disk seek costs roughly $10^5$ to $10^6$ times what an in-memory comparison costs. A binary tree and a B-Tree can both be “$O(\log n)$,” yet one needs 30 seeks per query and the other needs 4 — a difference that dwarfs any constant-factor optimization done inside a single node. This is also why B-Trees choose $m$ to match the page size of the storage medium: the goal is not the smallest possible tree in the abstract, but the fewest possible page transfers in practice.
11. Applications & Alternatives
Where B-Trees and B+ Trees show up
- Relational database indexes: PostgreSQL’s default B-tree index type and MySQL/InnoDB’s clustered and secondary indexes are both B+ Trees, chosen specifically for efficient range queries and ordered scans.
- Filesystem directory structures: NTFS directories are B+ Trees keyed on filename; ext4 uses an HTree (a specialized hashed B-tree variant) for large directories, for the same reason — fast lookup and ordered traversal on disk.
- In-memory hash indexes, by contrast: a hash index gives $O(1)$ average point lookups but has no notion of key order, so it cannot answer
WHERE age BETWEEN 20 AND 30without scanning every entry; a B+ Tree answers the same query in $O(\log_m n + k)$ by walking a contiguous run of the leaf chain. - LSM-trees: write-heavy engines (LevelDB, RocksDB, Cassandra) favor log-structured merge trees over B-Trees for their better write throughput, trading some read amplification for it — a reminder that B-Trees are not universally optimal, only optimal for read-heavy, seek-sensitive, range-query workloads.
The decision is always about the dominant operation. If the workload is point lookups only, with no interest in ordering, a hash index is simpler and asymptotically just as fast. The moment range queries, ordered iteration, or disk locality matter, a B+ Tree’s combination of low height and a linked leaf layer becomes hard to beat.
12. Exercises
Easy
- Given a B-Tree with root
[20, 40]and leaf children[10, 15],[25, 30, 35],[45, 50], trace the search for35and for18by hand, listing every node visited and every comparison made.
Medium
- Using the B-Tree insert implementation from Section 6 as given, implement
search(tree, k)as a method on the sameBTreeclass (rather than the free function from Section 5), and verify it finds every key the insert trace placed into the tree from Section 3.
Hard
- Implement a B+ Tree range query
range_query(root, lo, hi)that returns all keys $k$ with $lo \le k \le hi$, using only the leaf-linked-list walk shown in Section 9 — no re-descending into the tree once the starting leaf is found. Test it against a brute-force scan over the same keys to confirm the results match for several random $(lo, hi)$ pairs.
WHERE timestamp BETWEEN t1 AND t2. Choose B-Tree versus B+ Tree, justify the order $m$ in terms of a realistic disk page size, estimate the resulting tree height, and explain precisely why a hash index or a plain binary search tree would perform worse on this workload.Review rhythm: tomorrow, redraw the invariants from memory and re-derive why splitting happens on the way down rather than after an overflow; next week, implement B-Tree delete from scratch without looking at this page, then check it against the borrow and merge traces here; several weeks later, explain to someone else why a database index is a B+ Tree and not a binary search tree, using disk seeks (not Big-O alone) as the argument.
13. Summary Cheat Sheet
- Binary search trees are $O(\log_2 n)$ tall; B-Trees of order $m$ are $O(\log_m n)$ tall — far fewer disk seeks for the same $n$.
- Every non-root node holds between $t-1$ and $2t-1$ keys; every leaf sits at the same depth.
- Insert splits a full node before descending into it, so one downward pass suffices with no separate rebalancing phase.
- Delete guarantees the child about to be entered has at least $t$ keys, by borrowing from a sibling or merging with one.
- B+ Trees keep all data in the leaves, with internal nodes storing routing keys only, and leaves linked in sorted order.
- That leaf-linked-list is what makes range scans $O(\log_m n + k)$ instead of requiring repeated tree traversal.
- Databases and filesystems use B+ Trees because minimizing height directly minimizes disk seeks, and range queries are common.