Indexing & Storage Engines

How databases find rows fast, why write-heavy stores are built inside-out, and what every index costs you on write.

B-trees LSM trees Read vs write amp Composite indexes Covering indexes
๐ŸŒณ

B-Tree Indexes

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

A 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

The 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-treeLSM tree
WritesRandom I/O, in place โ€” slowerSequential, buffered โ€” fast
ReadsOne path down the tree โ€” predictableMay check several SSTables
Write amplificationUpdate the page each writeRewritten repeatedly by compaction
Read amplificationLowHigher (mitigated by Bloom filters)
SpaceFragmentation, some slackBetter compression; stale copies until compaction
Optimized forRead-heavy, range queriesWrite-heavy ingest
Found inPostgres, MySQL (InnoDB)Cassandra, RocksDB, LevelDB
๐Ÿ’ก
Tie the engine to the workload out loud. "This is write-heavy ingest with mostly key lookups, so an LSM-based store fits โ€” sequential writes, Bloom filters keeping reads cheap. If it were read-heavy with range scans and joins, I'd want a B-tree engine." That one sentence shows you know what's under the database, not just its name.
๐Ÿงญ

Index Strategy

Beyond single-column indexes, a few patterns solve most real query problems โ€” and a few mistakes make the planner skip your index entirely.

Composite indexes column order matters

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.

Covering indexes index-only reads

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.

โš ๏ธ
The planner silently ignores an index when it can't use it: a leading wildcard (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

An 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.

โ„น๏ธ
"Index everything" is a write-heavy anti-pattern. On an ingest-heavy table, each extra index drags throughput down measurably. Index the columns your real queries filter and sort on, drop the ones nothing uses, and remember the read/write trade is the same one running through the whole system โ€” you're moving work from read time to write time (Tradeoffs).
๐Ÿ“‹

Quick Reference

B-treeLSM tree
Best atReads, range scansWrite-heavy ingest
WritesRandom, in placeSequential, buffered
Read costLow, predictableMultiple SSTables + Bloom filters
EnginesPostgres, InnoDBCassandra, RocksDB
Index typeSpeeds upCosts
Single-columnLookups and sorts on one fieldWrite overhead per index
Composite (a,b,c)Queries on left-prefix of the columnsUseless for non-prefix columns
CoveringIndex-only reads, no table hitLarger index, more write cost
PartialQueries over a subset of rowsOnly helps matching predicates
โ„น๏ธ
The one-liners to keep ready. B-tree for read-heavy and ranges; LSM for write-heavy ingest, with Bloom filters and compaction paying down the read tax. Composite index order follows the query's left-to-right filters; covering indexes dodge the table entirely. Every index is a second copy maintained on every write โ€” index what queries use, not everything.