A disk-resident index holds millions of ordered records and must answer two shapes of query cheaply: “find key K” and “read every key between A and B in order.” A plain B-tree answers the point lookup in a handful of page reads, but the range query forces an in-order traversal that repeatedly climbs back into internal nodes to find the next key — random I/O proportional to the levels crossed, not to the rows returned.

The B+ tree is the B-tree variant that reshapes the node layout for exactly that second query. Every (key, value) pair moves down to the leaves; internal nodes keep only keys, acting as a routing index whose separators point to the child subtree that owns a range. A separator can equal a key still living in a leaf — it is a signpost, not the record. Then the leaves are chained into a linked list (next, usually also previous), so once a descent lands on the first matching leaf, the scan walks the chain sequentially in key order without touching an internal node again.

Because internal nodes carry no values, each routing page packs far more separators than a leaf packs records. Fan-out rises, the tree gets even shallower than a B-tree over the same data, and the routing levels tend to stay cached in the buffer pool. This is the structure that RDBMS indexes and filesystem B-trees actually ship.

Core shape: all (key, value) pairs in the leaves → internal nodes are a routing key index → leaves linked in sorted order → one descent then a sequential leaf walk answers a range → O(n) storage.

Visualization pending

Planned StepTrace: a tree card showing internal nodes holding only routing keys, all values living in the leaves, and the leaves chained left-to-right so a range scan walks the leaf list after a single descent. No matching renderer exists in engine.js yet.

Representation

Two node kinds share one page-sized layout:

  • An internal node holds k separator keys and k + 1 child pointers. Separator s_i guarantees every key in child i is < s_i and every key in child i + 1 is >= s_i. It stores no values and no leaf-record pointers.
  • A leaf node holds the actual (key, value) entries (or, for a non-clustered index, the key plus a row pointer) in sorted order, plus a next pointer to its right sibling and typically a prev pointer to its left one.

Three invariants define a valid state:

  1. All leaves sit at the same depth; every search path has identical length.
  2. A separator in an internal node may duplicate a key held in some leaf. The internal copy exists only to route the descent; deleting the leaf record does not require removing the separator, so a routing key can outlive its value.
  3. The leaf chain is a total order: following next from the leftmost leaf visits every key in ascending order exactly once.

A point lookup compares against separators to pick a child at each level and always continues to a leaf, because that is the only place a value exists. A range scan [A, B] descends once to the leaf holding A, reads forward within the leaf, then follows next pointers until a key exceeds B. The descent cost is the tree height; the walk cost is proportional only to the number of matching entries.

Complexity

Bounds are counted in page I/Os with m the node fan-out (keys per page), which is large for disk pages, so the tree is shallow. k is the number of entries a range scan returns.

OperationTime (page I/Os)Structure spaceCause
Search (point lookup)O(log_m n)O(n)One descent to a leaf; internal levels usually cache-resident.
InsertO(log_m n)O(n)Descend to a leaf, insert in order, split and push a separator up on overflow.
DeleteO(log_m n)O(n)Descend to a leaf, remove, borrow or merge with a sibling on underflow.
Range scan [A, B]O(log_m n + k)O(n)One descent finds A, then k entries are read by following leaf next links — no re-ascent per key.
Ordered full scanO(log_m n + n)O(n)Descend to the leftmost leaf, then walk the entire chain sequentially.

The + k term is the whole point: the range scan pays the tree height once and then reads matches as a sequential walk of the linked leaves, so cost tracks result size rather than tree structure. A B-tree lacking leaf links pays roughly O(k log_m n) for the same range because it re-descends to locate each successor. High fan-out keeps log_m n at two to four levels for realistic table sizes, and the top levels stay in memory, so an isolated lookup often costs a single leaf read.

Boundaries

A point lookup always reaches a leaf. A plain B-tree can find its value at an internal node and stop one or more levels early; the B+ tree cannot, because internal nodes hold no values. The trade is a slightly deeper worst-case path for a single key in exchange for uniform lookup latency — every key costs one full descent — and the cheap range scans the design exists to provide. Because the routing level is smaller and usually cached, the extra descent rarely translates into extra physical I/O.

Leaf-link maintenance rides on top of the ordinary split and merge logic. When a leaf splits, the new leaf must be stitched into the chain: the original leaf’s next is repointed at the new leaf, and the new leaf’s next inherits the old target (and the reverse pointers updated when the list is doubly linked). A merge does the mirror — the survivor absorbs the neighbor’s entries and relinks past the removed node. Getting this relinking wrong corrupts ordered iteration without breaking point lookups, so the defect can hide until a range scan skips or repeats a run of keys.

The same page-sizing constraint as a B-tree applies: node capacity is chosen so a node fills one storage page. Oversized keys or values lower fan-out, raise the tree, and erode the shallow-tree advantage. Variable-length keys and prefix compression in real implementations exist to keep separators small and fan-out high.

Reference drawer

Questions

References