B-Tree Indexes
the default, and why lookups are O(log n)Without an index, finding a row means scanning the whole table. A B-tree index is a balanced, sorted tree that turns that scan into a walk down a handful of levels. Each node holds many keys and points to children; you descend, narrowing the range at each step, until you land on the row. Because the tree stays balanced and wide, even a billion rows sit only three or four levels deep โ that's the O(log n).
Looking up key = 42:
[ 30 | 60 ] root
/ | \
[10|20] [40|50] [70|80] descend into the 30โ60 child
โ
โ 42 found ~3โ4 levels even for billions of rows
Keys are kept SORTED, so range scans are cheap too:
"everything between 40 and 55" = land on 40, walk right.
Sorted order is the second gift: range queries (BETWEEN, ORDER BY, prefix matches) walk the leaves in order instead of jumping around. The cost lands on writes โ every insert, update, and delete must keep the tree sorted and balanced, so each additional index is more work on every write (Tradeoffs).
LSM Trees
why write-heavy stores are built differentlyA B-tree writes in place, which means random disk I/O on every write. An LSM tree (log-structured merge tree) flips this: buffer writes in memory, then flush them to disk in big sequential batches. Sequential I/O is far faster than random, so writes fly. This is why Cassandra, RocksDB, and LevelDB exist (wide-column stores).
WRITE PATH READ PATH
write โโถ memtable (in RAM, sorted) check memtable โโถ hit? done
โ when full, flush miss โโถ check SSTables newestโoldest
โผ (sequential write) โ a read may touch many files
SSTable SSTable SSTable โผ
(immutable, on disk) Bloom filter per SSTable says
โ "definitely not here" โ skip the file,
โผ compaction merges avoid a useless disk read
fewer, bigger SSTables
The tax is on reads. Since data is spread across many immutable SSTables, a read may check several of them. Two things blunt that: compaction periodically merges SSTables into fewer, larger ones, and a Bloom filter per SSTable answers "is this key definitely not here?" so reads skip files that can't contain the key. Bloom filters can say "maybe present" falsely but never "absent" falsely, which is exactly what you need to avoid pointless disk reads.
B-Tree vs LSM
read-optimized vs write-optimizedThe choice is a trio of amplifications โ the same write shows up as extra I/O, extra reads, or extra space, and you can't minimize all three at once.
| B-tree | LSM tree | |
|---|---|---|
| Writes | Random I/O, in place โ slower | Sequential, buffered โ fast |
| Reads | One path down the tree โ predictable | May check several SSTables |
| Write amplification | Update the page each write | Rewritten repeatedly by compaction |
| Read amplification | Low | Higher (mitigated by Bloom filters) |
| Space | Fragmentation, some slack | Better compression; stale copies until compaction |
| Optimized for | Read-heavy, range queries | Write-heavy ingest |
| Found in | Postgres, MySQL (InnoDB) | Cassandra, RocksDB, LevelDB |
Index Strategy
composite, covering, and when the planner ignores youBeyond single-column indexes, a few patterns solve most real query problems โ and a few mistakes make the planner skip your index entirely.
An index on (a, b, c) is sorted by a, then b, then c โ like a phone book by last then first name. It serves queries on a, a+b, and a+b+c, but not on b alone. Put the column you always filter on first, the range column last.
If the index already contains every column a query needs, the database answers from the index alone and never touches the table. A huge win for hot queries โ you're reading one structure instead of two.
LIKE '%term' can't seek a sorted tree), a function on the column (WHERE lower(email)=โฆ unless you indexed that expression), or low selectivity (a boolean flag where a full scan is cheaper than the index round trips). "I added an index" isn't enough โ it has to be one the query can actually use.The Write Cost of Indexes
every index is a second copy to maintainAn index is not free storage that only helps. It's a separate sorted structure the database must update on every write to the indexed column. Five indexes on a table means one INSERT becomes six writes: the row, plus each index. Indexes speed reads and slow writes, always.
Quick Reference
engines and index types on one card| B-tree | LSM tree | |
|---|---|---|
| Best at | Reads, range scans | Write-heavy ingest |
| Writes | Random, in place | Sequential, buffered |
| Read cost | Low, predictable | Multiple SSTables + Bloom filters |
| Engines | Postgres, InnoDB | Cassandra, RocksDB |
| Index type | Speeds up | Costs |
|---|---|---|
| Single-column | Lookups and sorts on one field | Write overhead per index |
| Composite (a,b,c) | Queries on left-prefix of the columns | Useless for non-prefix columns |
| Covering | Index-only reads, no table hit | Larger index, more write cost |
| Partial | Queries over a subset of rows | Only helps matching predicates |