How Database Indexes Work (B-Tree)

Lab
Easy
Medium
Hard

Lookup with an indexEasy

A B-tree finds the row in just a few node reads.

1 · The index on users

CREATE INDEX idx_email ON users (email);

2 · The query

SELECT * FROM users WHERE email = 'rhea@mail.com';

3 · Pretend the table is bigger

Rows in the real table1M

The picture below uses 24 rows so you can follow it. The cost panel extrapolates to this size.

1 / 50 nodes · 0 rows
Index Scan using (email)
Use the B-tree index. The planner picks (email). It will walk the tree instead of reading every row.

What the database does

EXPLAIN → Index Scan using (email)
B-tree index on (email)
root N9
priya@mail.com
→ N7 N8
node N7
eli@mail.com
ivan@mail.com
mara@mail.com
→ L1 L2 L3 L4
node N8
tara@mail.com
→ L5 L6
leaf L1→ L2
aman@mail.com ↦1
bela@mail.com ↦2
chen@mail.com ↦3
dara@mail.com ↦4
leaf L2→ L3
eli@mail.com ↦5
farah@mail.com ↦6
gita@mail.com ↦7
hugo@mail.com ↦8
leaf L3→ L4
ivan@mail.com ↦9
jana@mail.com ↦10
kavi@mail.com ↦11
lena@mail.com ↦12
leaf L4→ L5
mara@mail.com ↦13
mason@mail.com ↦14
nina@mail.com ↦15
omar@mail.com ↦16
leaf L5→ L6
priya@mail.com ↦17
quinn@mail.com ↦18
rhea@mail.com ↦19
sam@mail.com ↦20
leaf L6
tara@mail.com ↦21
uma@mail.com ↦22
vik@mail.com ↦23
wen@mail.com ↦24
Table “users” (24 of 1M rows) read match
1 Aman · IN · 21
2 Bela · IN · 32
3 Chen · US · 43
4 Dara · IN · 54
5 Eli · UK · 27
6 Farah · IN · 38
7 Gita · DE · 49
8 Hugo · US · 22
9 Ivan · IN · 33
10 Jana · IN · 44
11 Kavi · US · 55
12 Lena · IN · 28
13 Mara · UK · 39
14 Mason · IN · 50
15 Nina · DE · 23
16 Omar · US · 34
17 Priya · IN · 45
18 Quinn · IN · 56
19 Rhea · US · 29
20 Sam · IN · 40
21 Tara · UK · 51
22 Uma · IN · 24
23 Vik · DE · 35
24 Wen · US · 46
Cost at 1M rows (estimate)
Full table scan
400.0 ms
Index Scan using (email)
250 µs

A B-tree for 1M rows is only about 3 levels deep, so a lookup reads ~3 pages instead of 10K pages.

✓ The index is used

The condition matches the leading column(s) of the index.

Work done: 3 index node reads + 1 table row reads, for 1 matching row.

Learn the theory

How Database Indexes Work (B-Tree): how it works

5 min read · Updated

Without an index a database answers a query the only way it can: by reading every row. An index is a separate, sorted structure — usually a B-tree — that lets it jump straight to the matching rows, the way a book's index saves you from reading every page. It is the single most effective performance tool in SQL, and understanding it explains why some queries fly while others crawl.

In short

  • No index → sequential scan: cost grows linearly with table size.
  • A B-tree index finds a row in about log(N) page reads — roughly 3–4 even for hundreds of millions of rows.
  • Indexes are sorted, so they also serve ranges, ORDER BY and prefix searches (LIKE 'Ma%').
  • A composite index (a, b) helps queries that filter on a, or on a and b — the leftmost prefix — but not on b alone.
  • Indexes aren't free: every write must update them, and the planner ignores them when a query matches a large share of the table.

Full table scan vs index lookup

A table is stored as unordered pages of rows. To find email = 'rhea@mail.com' with no index the database must read and test every row (a sequential scan). With an index on email it descends a tree that is sorted by email, reads a few small pages and follows a pointer to the single matching row.

EXPLAIN SELECT * FROM users WHERE email = 'rhea@mail.com';
-- Seq Scan on users          (no index: reads every row)
-- Index Scan using idx_email (reads ~3 index pages + 1 row)

How a B-tree works

A B-tree (strictly a B+tree in most databases) keeps keys sorted in fixed-size pages:

  • The root and internal nodes hold separator keys that say “keys below X are in the left child, the rest in the right”.
  • The leaf nodes hold the actual keys in order, each with a pointer to its table row.
  • Leaves are linked to their neighbours, so a range scan finds the start once and then just walks sideways.
  • The tree stays balanced: every leaf is the same distance from the root, so every lookup costs the same.

Because each page holds hundreds of keys, the tree is extremely wide and shallow: about 200 keys per page means 4 levels cover 1.6 billion rows. That is why index lookups barely slow down as tables grow.

Ranges, ORDER BY and LIKE

  • BETWEEN, <, >: descend to the lower bound, then follow leaf links until the upper bound.
  • ORDER BY indexed_col: the data is already sorted, so no sort step is needed.
  • LIKE 'Ma%': a prefix is a range ('Ma' to 'Mb'), so the index works.
  • LIKE '%an' or LIKE '%an%': the beginning is unknown, so the sorted order is useless and the database falls back to scanning.

Composite indexes and the leftmost-prefix rule

An index on (country, age) is sorted by country first, then by age within each country. It's like a phone book sorted by last name, then first name: great for “last name = Smith and first name > M”, useless for “first name = Anna”.

Query filters onUses index (country, age)?
country = 'IN'Yes (leading column)
country = 'IN' AND age > 40Yes (both columns)
age > 40 onlyNo — it skips the leading column
country > 'A' AND age = 30Partly: only the range on country narrows the scan

Put equality columns first and the range column last, and order columns by how often queries filter on them.

Covering indexes (index-only scans)

Normally the index finds the row's location and the database then fetches the row from the table (a heap fetch), which is extra random I/O. If the index contains every column the query needs, the database can answer from the index alone: an index-only scan.

CREATE INDEX idx_country_age ON users (country, age);
SELECT country, age FROM users WHERE country = 'IN' AND age > 40;
-- Index Only Scan using idx_country_age   (table never touched)

When the planner ignores your index

  • Low selectivity. If a filter matches a big fraction of rows (say 30%+), hopping between index and table for each one is slower than one sequential scan. WHERE country = 'IN' on a column with four values often scans.
  • Tiny tables. Reading the whole table is cheaper than opening an index.
  • Stale statistics. The planner estimates row counts from statistics; run ANALYZE after big changes.
  • Type mismatches and functions that make the column not directly comparable.

Frequently asked questions

What is a database index?
A separate sorted data structure (usually a B-tree) that maps column values to row locations, letting the database find rows without scanning the whole table.
Why is a B-tree used for indexes?
It's balanced, keeps keys sorted, and is shaped to match disk pages: each node holds many keys, so the tree is only a few levels deep and every lookup, range scan and ordered read needs very few page reads.
What is the leftmost-prefix rule?
A composite index on (a, b, c) can be used for conditions on a, on a and b, or on a, b and c — but not for conditions on b or c alone — because the index is sorted by a first.
Why doesn't LIKE '%text' use an index?
A B-tree is sorted by the beginning of the value. With a leading wildcard the database can't know where to start in the sorted order, so it must examine every row. Use full-text search or a trigram index instead.
What is a covering index?
An index that includes all columns a query needs, so the database can answer from the index alone (an index-only scan) without reading the table.
Why would the database not use my index?
Common reasons: the query matches a large part of the table, the table is tiny, the condition applies a function to the indexed column, the leading column of a composite index isn't filtered, or statistics are out of date.

Keep exploring

More interactive labs

All labs

What Happens When 100 Reads Hit the Database

Connection pools, CPU workers, caches and replicas, as 100 requests arrive at once.

Databases12 min · Intermediate

SQL Isolation Levels Lab

Run two transactions side by side and watch dirty reads, phantoms and write skew appear.

Databases15 min · Advanced

JS Event Loop Visualizer

See the call stack, microtasks and timers run, frame by frame.

JavaScript10 min · Intermediate

CORS & Preflight Simulator

Edit the request and the server headers, then see exactly why the browser blocks it.

Networking12 min · Intermediate

HTTP Caching Explained

Cache-Control, ETag and 304s, one request at a time.

Networking12 min · Intermediate

What Happens When You Type a URL

DNS, TCP, TLS and HTTP — with a stopwatch.

Networking10 min · Beginner

Sharding & Replication

Split data across servers, copy it for reads, and see what each choice costs.

Databases15 min · Advanced

Redis Commands & Data Structures

Strings, lists, hashes, sets and sorted sets, one command at a time.

Redis15 min · Beginner

Redis Caching: Cache-Aside, TTL & Eviction

Hit or miss? Watch LRU, LFU, TTLs and a cache stampede decide your database load.

Redis12 min · Intermediate

Webhooks: Delivery, Retries & Signatures

Why you get duplicate deliveries, lost events and forged requests — and how to stop them.

Queues & Jobs12 min · Intermediate

Cron Jobs: Schedules, Overlaps & Missed Runs

Write a cron expression and watch what really happens when jobs overlap or the scheduler is down.

Queues & Jobs10 min · Beginner

Job Queues with BullMQ (Redis)

Watch jobs move through waiting, active, delayed, completed and failed — and see what BullMQ stores in Redis.

Queues & Jobs14 min · Intermediate

RabbitMQ: Exchanges, Queues, Acks & Dead Letters

Direct, topic and fanout routing, competing consumers, prefetch, redelivery and dead-letter queues, drawn.

Queues & Jobs14 min · Intermediate

Git Commands, HEAD & Refs (Visual)

Every core git command drawn with arrows: three areas, commits, branches, HEAD and remotes.

Git15 min · Beginner

Git Merge vs Rebase

Watch the commit graph change under merge, rebase and squash.

Git10 min · Beginner