sehaj/learn
System design/04 Databases and data modeling

04.02 · with code · 13 min

Indexes and B-trees

Why finding one row among 600 million takes a handful of disk reads, how a B-tree index works, which column order to choose, and what every index costs on writes.

The problem: finding one row

Snip's links table will hold about 600 million rows after five years (01.05 Back-of-the-envelope estimation). An owner opens their dashboard and Snip runs:

sql
SELECT code, long_url, created_at
FROM links
WHERE owner_id = 42
ORDER BY created_at DESC
LIMIT 50;

Without help, the database has only one way to answer: read every row in the table, keep the ones where owner_id = 42, then sort them. That's a full table scan: about 300 GB read to return 50 rows. At SSD speeds that's around 15 seconds, for every dashboard load.

An index is a sorted copy with a map

An index is a separate structure that keeps one or more columns in sorted order, each entry pointing at its row. Because it's sorted, the database can jump to the right place instead of reading everything, the way you'd find a word in a dictionary.

Almost every relational database uses a B-tree for this. Data on disk is read in pages (8 KB in Postgres), so a B-tree packs hundreds of sorted keys into each page:

  • The root page holds a few hundred keys, each pointing to a page on the next level down.
  • Each inner page splits its range further.
  • The leaf pages hold every key in order, each with a pointer to its row.

Because each level multiplies by hundreds, the tree is very shallow. Three hundred keys per page means three levels cover 27 million rows and four cover 8 billion. Finding any one row takes about four page reads, and the upper levels are usually already in memory. Try it with Snip's table:

LabHow deep is the index?

The index, level by level

Root1 page
Level 223 pages
Level 36.67K pages
Leaves2M pages

Pale levels fit in 1 GB of memory and are usually cached.

Find one row by index

5

page reads · ~80 µs

2 from disk, the rest cached

Find it by full scan

37.5M

page reads · ~15 s

307 GB read

Try this

Column order matters

An index on several columns sorts by the first, then by the second within each first value, and so on, like a phone book sorted by surname, then first name. That decides which queries it can help:

sql
CREATE INDEX links_owner_created ON links (owner_id, created_at DESC);
QueryUses this index?
WHERE owner_id = 42 ORDER BY created_at DESC LIMIT 50Yes, perfectly: jump to owner 42, read the first 50 entries, already sorted.
WHERE owner_id = 42Yes: the leftmost column alone is fine.
WHERE created_at > now() - interval '1 day'No: dates are only sorted within each owner. Like finding everyone named "Ana" in a phone book sorted by surname.

The rule: an index helps a query that filters on a leftmost prefix of its columns. Put columns compared with = first, then the column you sort or range over.

If an index also contains every column a query needs, the database can answer from the index alone without visiting the table at all. That's a covering index, and it saves a page read per row.

What indexes cost

Indexes make reads fast by making writes slower:

  • Every insert updates every index. A table with six indexes does seven writes for each new row, and an update to an indexed column changes the index too.
  • Storage. Each index is a sorted copy of its columns; several indexes can rival the table in size.
  • Memory. Hot indexes want to stay in RAM. Unused indexes still compete for it.

So index for the queries you actually run, check that each index is used, and drop the ones that aren't. Snip's links table needs just two: the primary key on code (for redirects) and (owner_id, created_at) for dashboards.

B-trees also handle ranges and sorting (created_at > …, ORDER BY), which is why they're the default. Hash indexes find exact matches only, and specialised indexes exist for text search, locations and arrays.

Seen in production

SQLite

2026

Query planner

SQLite's documentation walks through a lookup step by step. Without an index the query is a full table scan whose time is proportional to the number of rows. With an index it becomes a binary search, so the time is proportional to log N, and a query returning K rows costs about (K+1)·log N. When an index contains every column the query needs, SQLite never consults the table, which the page describes as roughly doubling the speed.

What we foundThe cost of a lookup drops from proportional to N (a full scan) to proportional to log N with an index, and covering indexes skip the table entirely.

Query Planningsqlite.org ↗
Predict first

Snip has an index on links (owner_id, created_at). A new admin page runs: SELECT * FROM links WHERE created_at > now() - interval '1 hour'. Will the index help?

Commit to an answer before you read on.

Recap

Interview questions

JuniorfreqMust-knowConcept

How does a database index work, and what does it cost?

Report a mistakeEdit this page