Skip to main content
databases

B-trees

11 min read
Fully authored

The data structure inside most relational databases.

In 1970, two Boeing researchers — Rudolf Bayer and Edward McCreight — were thinking about magnetic disks. Disks were slow. Every access needed a seek — the read head physically moving to the right track — costing 10-20ms. Memory was tiny. To index a large data set on disk, you needed a data structure that would let you find any record with as few disk seeks as possible.

Their answer: a tree structure where every node holds many keys, not just one or two. Each disk block became a tree node. A single seek loaded 100 keys instead of one. The tree grew wide instead of tall — a database of 1 billion rows needed only 4-5 seeks to reach any record. Bayer and McCreight called it the B-tree.

54 years later, the B-tree still runs the world. Every serious relational database — PostgreSQL, MySQL/InnoDB, Oracle, SQL Server, SQLite, LMDB — stores its indexes as B-trees (or the closely-related B+tree variant). Every unique constraint, every foreign key check, every ORDER BY, every WHERE clause on an indexed column ultimately walks a B-tree. If you understand one on-disk data structure deeply, make it this one.

The paper

Bayer & McCreight (1972) — "Organization and Maintenance of Large Ordered Indices." Acta Informatica. The 6-page paper that defined B-trees. Turing Award (2001 Bayer) recognized his contributions to database indexing. The follow-up Comer (1979) — "The Ubiquitous B-tree" (Computing Surveys) — surveyed the many variants (B+, B*, R-tree) that grew from the original.

The insight — wide nodes, shallow tree

A binary tree is the wrong shape for disk. A perfectly balanced binary tree with 1 billion keys is 30 levels deep. Fetching a key means 30 seeks — 30 × 10ms = 300ms. Unacceptable. The B-tree solves this by putting many keys per node (typically 100-500 keys per 8 KB page). With 200 keys per node, 1 billion records fits in ⌈log₂₀₀(10⁹)⌉ ≈ 4 levels. Total: 4 seeks. That's the difference between a database and a toy.

Why B-tree beats binary tree for disk
Binary tree (bad for disk)
30 levels for 1 billion records.
30 seeks × 10ms = 300ms per lookup.
B-tree (great for disk)
50 · 100 · 150 · 200
4-5 levels for 1 billion records.
5 seeks × 10ms = 50ms per lookup.
The math: With 200 keys per node, log₂₀₀(10⁹) ≈ 4. Wide + shallow. Every seek reads 8 KB (200+ keys), so even the leaf-node scan is cheap.

The nodes are disk blocks — the smallest unit the OS reads from or writes to the disk. Standard block size is 4 KB or 8 KB. Every node access = one block read = one disk seek. The database keeps a small buffer pool of recently-used nodes in memory (Postgres's shared_buffers, InnoDB's buffer pool) so hot nodes stay hot. The root and top levels stay cached always; only leaves might miss.

Interactive: walk a B-tree search

A B-tree search is trivially simple. Start at root. Compare target key to node's separators. Pick the right child. Recurse until you hit a leaf. Watch it live:

B-tree search — step 1 of 3
Search for key 87.
50 · 100 · 15020 · 3565 · 75 · 90115 · 13510 · 1522 · 3055 · 6070 · 7280 · 85 · 87 · 9211587 < 100 → go left
Searching for key 87. Start at root, which holds separators [50, 100, 150]. 87 is between 50 and 100 → follow the middle child.

The magic — how inserts stay balanced

The hard part isn't search. It's inserts. If every insert appends to the same leaf, the tree becomes a list and search degrades to O(N). B-trees stay balanced through one rule: when a node gets full, split it into two and push the middle key up to the parent. If the parent overflows, split it too, recursively.

B-tree insert with split — step 1 of 5
Node capacity = 4 keys. Watch what happens on overflow.
4010 · 20 · 30
Starting state: leaf holds [10, 20, 30]. Node capacity: 4 keys. Room for one more. Parent holds separator [40].

The tree grows in height only when the root splits. This means the tree stays balanced automatically — no manual rebalancing, no O(N log N) worst case. Every insert, delete, or search costs O(log_M N) where M is the fanout. In practice for real databases, that's 4-6 disk seeks worst case, even for petabyte databases.

B+tree — the variant everyone actually uses

Real databases don't use plain B-trees. They use B+trees, a small but critical modification:

  • Internal nodes store only keys (used to guide the search).
  • Leaf nodes store all the data (or pointers to data rows).
  • Leaves are linked together in a doubly-linked list — so scanning "all records in order" is a single linear walk without going back up the tree.
B+tree — data in leaves, leaves linked in a list
50 · 100 · 150internal (keys only)10·25·35·45row1..row455·70·85·95row5..row8110·120·135·145row9..row12160·180·195row13..row15↔ leaves linked in a doubly-linked listRange scan: find start, walk right until end. No tree walk.

This is a massive win for range queries. SELECT * FROM users WHERE age BETWEEN 25 AND 40: find 25 in the leaves (O(log N) seeks), then just walk right until you pass 40. A million-row range scan reads sequentially from disk — orders of magnitude faster than random access.

Concurrency — the tricky part

With one thread it's simple. In production, hundreds of connections read and write concurrently. Two threads can't both split the same node — corruption. The classical solution: latch coupling. Acquire a lock on the root, descend to child, release the parent lock, acquire the child lock, and so on. If the child needs to split, upgrade back up the tree.

Modern databases use fancier techniques — optimistic latching, Blink-trees (Lehman & Yao 1981) that allow lock-free reads, copy-on-write B-trees (LMDB) that write new pages instead of modifying in place. The details matter for databases at high concurrency; for the mental model, know that B-tree implementations are highly optimized for parallel access.

B-tree vs LSM-tree — the fundamental storage-engine choice

DimensionB+treeLSM-tree
Optimized forReads (point lookups + range scans)Writes (append-only)
Write pathIn-place page update + WALAppend to memtable → flush to SSTable
Read pathOne traversal from root to leafCheck memtable + all SSTables (bloom filters help)
Write amplificationLow (rewrites one 8 KB page)High (compaction rewrites data 5-10x)
Read amplification1 tree walkMay need multiple SSTable reads
Space usage50-70% full pages typicalCompaction keeps overhead down but temp files spike
Range scansVery fast (walk linked leaves)OK (merge across SSTables)
Point lookupsO(log_M N) seeksMay check many SSTables (bloom filters help)
Real users (indexes)Postgres, MySQL, Oracle, SQL Server, SQLiteCassandra, HBase, RocksDB, LevelDB, ScyllaDB, TiKV

The trade-off is stark. B-trees are read-optimized: point lookups and range scans are cheap. Writes are expensive because they require in-place page updates and journal writes for durability. LSM-trees flip it: writes are cheap (append to a log-structured memtable, flush sequentially), reads are more expensive (may need to check multiple SSTables). Pick B-tree for read-heavy OLTP, LSM for write-heavy workloads.

Applied in real systems

PostgreSQL
Deep dive

PostgreSQL — the standard B+tree index

CREATE INDEX ... USING btree is the default (and usually the right answer). 8 KB pages. Multi-column keys. Includes ORDER BY optimization. Postgres also has GiST, GIN, BRIN, and Hash — but B+tree covers ~95% of production indexes.

Read the deep dive →
MySQL InnoDB
Deep dive

MySQL InnoDB — B+tree with clustered indexes

InnoDB stores the actual row data in the leaves of the primary-key B+tree — a "clustered index." Secondary indexes store the primary key as their leaf pointer. Trade-off: fast range scans by PK, slightly more overhead on secondary index lookups.

Read the deep dive →
SQLite
Deep dive

SQLite — the ubiquitous embedded B+tree

Every iOS app, Android app, Firefox, Chrome, Windows 10, macOS ships with SQLite. All storage is B+tree. Total deployments: probably the most-installed database in history. Single-file, zero-configuration, transactional.

Read the deep dive →
LMDB
Deep dive

LMDB — copy-on-write B+tree

Lightning Memory-Mapped Database. Symas Corporation. Reads are lock-free (MVCC via CoW). Backs OpenLDAP, Monero wallet. Reads are literally memory-map dereferences. Faster than any other embedded B+tree for read-heavy workloads.

Read the deep dive →
Oracle Database
Deep dive

Oracle — decades-old B+tree implementation

Oracle's B+tree code has been optimized since the 1970s. Supports every kind of composite index, function-based indexes, index-organized tables (like InnoDB clustered). The reference for enterprise B+tree tuning.

Read the deep dive →
BoltDB / BBoltDB
Deep dive

BoltDB — the Go-world B+tree

BoltDB (2013, Ben Johnson) — a pure-Go embedded B+tree inspired by LMDB. Its fork bbolt is what etcd (the Kubernetes datastore) uses to persist Raft log entries. Every Kubernetes cluster on Earth uses a B+tree underneath.

Read the deep dive →
WiredTiger (MongoDB)
Deep dive

MongoDB — WiredTiger's LSM+B+tree hybrid

MongoDB's storage engine WiredTiger supports both LSM and B+tree modes; B+tree is default for most collections. This is why MongoDB's point lookups feel fast — B+tree beats LSM for reads.

Read the deep dive →
Azure Cosmos DB
Deep dive

Cosmos DB — B+tree per partition

Cosmos DB physically partitions data. Within a partition, indexes are B+trees. Cross-partition queries do a scatter- gather across many B+trees. Global distribution + B+tree locality.

Read the deep dive →

Key takeaways

  • B-tree = wide, shallow, self-balancing tree designed for block-based disk storage. 4-6 levels for billion-row datasets.
  • Every relational database uses B+trees (Postgres, MySQL, Oracle, SQL Server, SQLite, LMDB) for indexes. 54 years old, still winning.
  • B+tree vs B-tree: B+ stores data only in leaves + links leaves in a list. Makes range scans a linear walk. Always the practical choice.
  • Node size = disk block size (typically 8 KB). Each seek fetches ~200-500 keys. Trade-off: bigger blocks = shallower tree but more wasted read for point lookups.
  • Inserts stay balanced via node splitting. The tree grows in height only when the root splits. O(log N) guaranteed.
  • Concurrency is hard. Latch coupling, Blink- trees, copy-on-write are the industrial techniques. Modern databases are highly optimized.
  • B-tree vs LSM-tree is the fundamental storage-engine choice: read-optimized (B+tree) vs write- optimized (LSM). Pick based on workload.

References

  • Bayer & McCreight (1972) — Organization and Maintenance of Large Ordered Indices. Acta Informatica.
  • Comer (1979) — The Ubiquitous B-tree. ACM Computing Surveys.
  • Lehman & Yao (1981) — Efficient Locking for Concurrent Operations on B-trees (Blink-trees).
  • Graefe (2011) — Modern B-tree Techniques. Foundations and Trends in Databases.
  • Kleppmann (2017) Designing Data-Intensive Applications, Chapter 3.
  • Petrov (2019) Database Internals. Chapter 2 has the best modern treatment.

Practice what you just read

Every foundation concept has a companion quiz to close the loop.