Chapter 14: B-Tree Internals — The Data Structure Behind Indexes

The B-Tree (and its variant B+Tree) is the most important data structure in databases. Understanding it explains why indexes are fast, why column order in composite indexes matters, and why some queries can't use indexes.

Why B-Trees? (Not Binary Trees)

A binary search tree has 2 children per node. For 1 million keys, that's ~20 levels deep = 20 disk reads per lookup. Too slow.

A B-Tree has hundreds of children per node (because each node = one disk page = 8KB, fitting many keys). For 1 million keys with a branching factor of 500, that's only 3 levels deep = 3 disk reads.

Binary Tree (depth 20 for 1M keys): B-Tree (depth 3 for 1M keys): o [k1|k2|...|k499] / \ / | | \ o o [...] [...] [...] [...] / \ / \ / | \ o o o o [leaf][leaf][leaf] ...20 levels... = 20 disk reads = 3 disk reads!

B+Tree Structure (What Databases Actually Use)

Databases use B+Trees (a variant where all data is in leaf nodes):

┌─────────────────────┐ │ [30 | 60 | 90] │ ← Internal (root) node │ ↓ ↓ ↓ ↓ │ Only keys + child pointers └───┼────┼────┼───┼───┘ / │ │ │ \ ┌────────┘ │ │ │ └────────┐ ▼ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ 1|5|12|18|25 │→│31|35|42|55│→│61|67|78|85│→│91|93|95|98|99│ │ ↓ ↓ ↓ ↓ ↓ │ │ ↓ ↓ ↓ ↓ │ │ ↓ ↓ ↓ ↓ │ │ ↓ ↓ ↓ ↓ ↓ │ │ row pointers │ │row ptrs │ │row ptrs │ │ row pointers │ └──────────────┘ └──────────┘ └──────────┘ └──────────────┘ ← Leaf nodes: contain keys + row pointers, linked together → Key properties: • Internal nodes: only keys + child pointers (no data) • Leaf nodes: keys + row pointers (or full rows in clustered index) • Leaf nodes are linked (→) for efficient range scans • All leaves at same depth (balanced)

Operations

Lookup: O(log n)

// Find key=42:
// 1. Root: 42 > 30, 42 < 60 → go to second child
// 2. Internal: find leaf containing 42
// 3. Leaf: binary search within leaf → found at position 3
// 4. Follow row pointer → read actual row from heap
// Total: 3-4 page reads

Range Scan: O(log n + k)

// Find all keys between 35 and 78:
// 1. B-tree lookup to find leaf containing 35 (3 reads)
// 2. Follow leaf links → → → until key > 78
// 3. Collect all row pointers along the way
// This is why leaf nodes are linked!

Insert: O(log n)

// Insert key=40:
// 1. Find correct leaf (same as lookup)
// 2. Insert into leaf (maintaining sorted order)
// 3. If leaf is full → SPLIT into two leaves
//    → promote middle key to parent
//    → if parent full, split recursively up

Composite Index Column Order

A composite index on (country, city, name) sorts like a phone book: first by country, then by city within each country, then by name within each city.

Index on (country, city, name): Leaf nodes (sorted): [Finland, Helsinki, Alice] → row [Finland, Helsinki, Bob] → row [Finland, Turku, Charlie] → row [Sweden, Gothenburg, Dave] → row [Sweden, Stockholm, Eve] → row [Sweden, Stockholm, Frank] → row This index can efficiently answer: ✓ WHERE country = 'Sweden' (leftmost prefix) ✓ WHERE country = 'Sweden' AND city = 'Stockholm' (two leftmost) ✓ WHERE country = 'Sweden' AND city = 'Stockholm' AND name = 'Eve' ✓ WHERE country = 'Sweden' ORDER BY city (sorted within country) This index CANNOT efficiently answer: ✗ WHERE city = 'Stockholm' (skipped country — not leftmost) ✗ WHERE name = 'Eve' (skipped country and city) ✗ ORDER BY city (without country filter)
⚠️ Leftmost Prefix Rule

A composite index on (A, B, C) can be used for queries on: A, (A,B), or (A,B,C). It CANNOT be used for queries on just B, just C, or (B,C). The leftmost columns must be present. This is the most common indexing mistake.

Index Types Beyond B-Tree

TypeBest ForExample
B-TreeEquality, range, sorting (default)WHERE age > 25
HashEquality only (faster than B-tree for =)WHERE id = 123
GiSTGeometric, full-text, rangesWHERE location @> point
GINArrays, JSONB, full-text searchWHERE tags @> '{grpc}'
BRINLarge tables with natural orderingTime-series data (sorted by timestamp)

Clustered vs Non-Clustered

AspectClustered (InnoDB PK)Non-Clustered (Secondary)
Leaf containsFull row dataKey + pointer to row
Table orderRows stored in index orderSeparate structure
Count per tableOne (the PK)Many
Range scanVery fast (sequential I/O)May need random I/O to fetch rows
Key Takeaways