Skip to content

Indexing and Optimization | Databases

Without an index, finding a specific row in a table of NN rows requires a full sequential scan, Which is O(N)O(N). A B-tree index reduces this to O(logN)O(\log N) — for a table of one billion rows, that Is the difference between examining one billion rows and approximately 30.

Indexes are the single most impactful performance tool available to a database user. The query Planner cannot use an index that does not exist, and adding the wrong index wastes storage and slows Down writes. Understanding how indexes work internally is the difference between a query that runs In milliseconds and one that takes minutes.

The B-tree (Bayer and McCreight, 1972) is the default index structure in PostgreSQL, MySQL (InnoDB), And most relational databases. Despite the name, modern implementations use B+ trees.

A B-tree is a balanced, self-sorting tree with the following properties:

  • Every node stores an array of keys and pointers
  • Internal nodes store keys and pointers to child nodes
  • Leaf nodes store keys and pointers to the actual table rows (or to the row”s physical location)
  • The tree is always balanced: all leaf nodes are at the same depth
  • Every node (except the root) is at least half full (this is the minimum fill factor)
B-Tree (internal nodes store data):
[10 | 20 | 30]
/ | | \
[3,5,7] [12,15,18] [22,25,28] [32,35,40]
B+ Tree (all data in leaves, leaves linked):
[10 | 20 | 30]
/ | | \
[3,5,7] [12,15,18] [22,25,28] [32,35,40]
| | | |
v v v v
[leaf] -> [leaf] -> [leaf] -> [leaf] (linked list for range scans)
PropertyB-TreeB+ Tree
Data storageIn all nodesOnly in leaf nodes
Leaf linkageNoneDoubly-linked list
Range scansRequires tree traversalSequential scan of linked leaves
Node fillVariable2/3 to 4/5 (higher fan-out)
HeightTaller for the same dataShorter (higher fan-out)

For a B+ tree with fan-out ff (number of children per internal node) and height hh:

\mathrm{Max entries = f^h

\mathrm{Height = \lceil \log_f N \rceil

In practice:

  • A PostgreSQL page is 8KB
  • A typical B-tree index entry is about 20-50 bytes (depending on key size)
  • Fan-out per internal node: approximately 160-400 entries
  • For 1 billion rows: height = log300(109)4\lceil \log_{300}(10^9) \rceil \approx 4

This means every index lookup traverses at most 4 pages — approximately 4 disk seeks or, more Likely with the buffer pool, 4 cache lookups.

  1. Find the correct leaf node by traversing from the root
  2. Insert the new key in sorted order in the leaf
  3. If the leaf overflows (exceeds page capacity):
  • Split the leaf into two halves
  • Promote the median key to the parent
  • If the parent overflows, split it recursively
  • If the root splits, create a new root (tree height increases by 1)
  1. Find the key in the leaf
  2. Remove the key
  3. If the leaf underflows (below minimum fill factor):
  • Attempt to redistribute entries from a sibling (borrow)
  • If siblings are also at minimum, merge with a sibling
  • If the parent underflows, recurse upward
  • If the root has one child and is an internal node, the child becomes the new root