
A sorted array is fast to find but slow to insert, because inserting shifts a huge contiguous run. The fix is to break the contiguity: instead of one big sorted line, split it into segments, and keep a small "index" that says roughly where each segment sits.
Let's build it in two steps: first a binary search tree (BST) to get the "insert without shifting" idea, then upgrade to a B+ tree — the thing databases really use.
A BST is a set of nodes. Each node holds a value and has at most two children: left = smaller, right = bigger. To insert 40 into [10, 20, 50], you don't shift anything — you compare and hang a new node:
20
/ \
10 50Search follows the same forks: at each node, go left if smaller, right if bigger. No shifting, ever. Insert and find are both O(height). If the tree stays balanced, height is O(log n).
But a bare BST has a fatal flaw: insert ordered data and it degenerates into a tall skinny line:
10 -> 20 -> 30 -> 40 -> 50 (height = n, search becomes O(n))That's the "insert fast, find slow" failure all over again. So we need the tree to stay balanced. That's the "B" part.
A B-tree's trick is: each node can hold many keys (not just one) and have many children (not just two). More keys per node → fewer levels → shallower tree → fewer steps to find.
Two rules keep it both ordered and compact:
The "B" is famously said to stand for the man who invented it (Bayer) — but the mnemonic "busy and shallow" is what actually matters.
A B+ tree is a B-tree with one crucial difference: all the data lives in the leaves, and the internal nodes hold only keys to steer (no data). And the leaves are linked left-to-right.
(internal, keys only, steer downward)
[ 40 | 80 ]
/ | \
[20][30] [50][60] [90][100] ← leaves hold real data
↑──────────┬───────────↑
linked left→rightWhy does that matter? Three things fall out:
The whole point of a B+ tree over a plain sorted array: you get sorted-range sweeping and cheap inserts, because the sorted data is broken into a chain of leaves, and inserts only touch one leaf (occasionally splitting it) instead of shifting the whole array.
Let's implement a simplified B+ tree — a leaf can hold up to MAX keys; when it overflows we split it. Search is the part that matters most for now.
// A simplified B+ tree just for searching (inserts use a simplified split)
class BPlusTree {
constructor(order = 2) {
this.order = order; // how many keys per leaf before splitting
this.root = { keys: [], children: [], isLeaf: true };
}
// find the leaf that could contain `key`
_findLeaf(node, key) {
if (node.isLeaf) return node;
// binary search node.keys to pick the child to descend into
let i = 0;
while (i < node.keys.length && key >= node.keys[i]) i++;
return this._findLeaf(node.children[i], key);
}
// is `key` in the tree?
contains(key) {
const leaf = this._findLeaf(this.root, key);
return leaf.keys.includes(key);
}
}You can see the shape of the whole thing even in this skeleton: you descend by comparing against a few keys per node, until you land on a leaf, then you just check the leaf.
Real insertion needs to split a full leaf (and propagate a key upward when a split happens). But the search — the thing that makes an index fast — is already visible: it's "binary search down the levels, then one scan of a leaf." That's O(log n) with a small constant.
Now connect it back to everything you've built:
Seq Scan.And here's the payoff you've been climbing toward: if there's a B+ tree index on books.id, then SELECT * FROM books WHERE id = 42 doesn't scan — it walks the tree in a few steps. In EXPLAIN output you saw that as Seq Scan turning into Index Scan. You just built the thing that makes that happen.
10,20,30,40,50 into a small tree and hand-trace contains(40) — how many nodes did you visit?30 <= price < 70 using the chain.Seq Scan into Index Scan.Keywords: B+ tree、balanced tree、leaf、internal node、range query、leaf-linked、O(log n)、Index Scan
You've built the structure that makes a query fast. But stepping back: a B+ tree index contains keys and pointers — and it's separate from the rows themselves. So what would you store in it to point at a row? And what if the rows are stored sorted, versus sitting in a heap?
That's Part 9 — where you'll discover that an index stores addresses (not the data), and that "address" is exactly the TID we keep circling. You're about to see the other half of the story.