Database Indexes
An index lets the database find rows without reading the whole table, turning seconds into milliseconds. Learn how B-tree and LSM-tree indexes work, composite indexes, and when an index makes things worse.
In a nutshell
- Without an index, a query reads every row (a full scan). An index is a sorted structure that jumps straight to matches.
- Most relational databases use B-trees: balanced, shallow, good for lookups and ranges. Write-heavy stores often use LSM trees.
- A composite index on (a, b) helps queries filtering on a, or on a and b, but not on b alone.
- Every index speeds up reads and slows down writes and uses storage. Index for your real queries, not every column.
Why indexes matter
Finding one customer by email in a table of 10 million rows without an index means checking all 10 million. With a B-tree index on email, the database walks a tree three or four levels deep and lands on the row. It is the same reason a book has an index at the back.
- 10,000,000rows checked by a full scan
- ~4tree levels walked with a B-tree index (with hundreds of keys per page)
Create an index, then check the plan uses it
CREATE INDEX idx_users_email ON users (email);
EXPLAIN SELECT id, name FROM users WHERE email = 'priya@example.com';
-- Look for an index scan on idx_users_email, not a sequential (full) scan.B-trees
A B-tree keeps keys sorted in fixed-size pages (often 8 or 16 KB). Each inner page holds hundreds of keys pointing to child pages, so the tree is very wide and very shallow. A lookup reads one page per level; a range query (WHERE created_at BETWEEN …) finds the start and then walks along the leaf pages in order.
Looking up email "priya@…" in a B-tree
Diagram as text
- Root page → Inner page
- Root page → Inner page: p ≤ key < t
- Inner page → Leaf page
- Leaf page → Table row: fetch
LSM trees
Databases built for heavy writes (Cassandra, RocksDB, and the storage engines inside many others) often use a log-structured merge tree. Writes go to an in-memory sorted table and an append-only log; when the memory table fills, it is flushed to disk as an immutable sorted file. Background compaction merges files and drops old versions.
| B-tree | LSM tree | |
|---|---|---|
| Writes | Update pages in place (random I/O) | Append and flush sequentially: very fast |
| Reads | One path down the tree: predictable | May check several files; Bloom filters help |
| Background work | Little | Compaction uses disk and CPU |
| Typical use | General-purpose relational databases | Write-heavy stores, time series, logs |
Composite and covering indexes
A composite index sorts by several columns, like a phone book sorted by last name, then first name. It helps queries that use its columns from the left.
One composite index, three queries
CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at);
-- Uses the index: filters on the first column
SELECT * FROM orders WHERE customer_id = 7;
-- Uses the index fully: first column equal, second column range, already sorted
SELECT * FROM orders WHERE customer_id = 7 AND created_at > '2026-01-01' ORDER BY created_at;
-- Cannot seek with it: skips the first column
SELECT * FROM orders WHERE created_at > '2026-01-01';A covering index contains every column the query needs, so the database answers from the index alone without visiting the table. Some databases let you add extra columns for this (INCLUDE in PostgreSQL and SQL Server).
When an index hurts
- Writes get slower: every insert, update and delete must also update every index on the table.
- Storage grows: indexes can be as large as the table.
- Low selectivity: an index on a column with two values (
is_active) rarely helps; reading half the table through an index is slower than a scan. - Functions hide the column:
WHERE LOWER(email) = …cannot use a plain index onemail(use an expression index, or store it normalised). - Leading wildcards:
LIKE '%gmail.com'cannot use a B-tree; consider a search engine or a reversed column.
Common mistakes
- Adding an index on every column "just in case", slowing every write.
- Building a composite index in the wrong column order for the queries that use it.
- Wrapping indexed columns in functions or casts in WHERE clauses.
- Guessing instead of reading the query plan.
Interview questions
Try answering out loud before you open each one.
A query got slow as the table grew. How do you investigate?
Run EXPLAIN ANALYZE to see whether it scans the full table and where time goes. Check which columns it filters, joins and sorts on, and add an index matching them (composite, in the right order, possibly covering). Re-check the plan. Also check for functions on columns and for returning far more rows than needed.
B-tree vs LSM tree?
B-trees update pages in place: predictable reads and good range scans, the default for relational databases. LSM trees append writes and merge files in the background: much higher write throughput, reads may touch several files (helped by Bloom filters), and compaction costs I/O. Choose LSM for write-heavy workloads.
Check yourself
Related topics
Further reading
- Use The Index, Luke: SQL indexing and tuning Markus Winand
- PostgreSQL documentation: Indexes PostgreSQL
Your notes
Saved in this browser as you type.
Finished this topic?
Mark it read to track your progress, then carry on with the next one.