system-design · intermediate

Database Storage Architectures — B-Trees vs. LSM-Trees

The Central Question

Consider two high-scale software applications running on the DataLab platform (datalab.com):


If both workloads use a traditional B-Tree storage engine (such as PostgreSQL's default engine):

Conversely, if both workloads use a Log-Structured Merge-Tree (LSM-Tree) storage engine (such as RocksDB or Cassandra):

This lesson answers one central question: How do B-Tree (read-optimized) and LSM-Tree (write-optimized) storage architectures organize memory and disk pages, handle write/read amplification, and dictate database performance under different workload access patterns?


The Core Trade-Off: Read Amplification vs. Write Amplification

Every database storage architecture makes a fundamental trade-off governed by disk physics:

flowchart LR
  subgraph B-Tree Storage Model: Read-Optimized
    BTree[In-Place Page Updates] -->|Low Read Amplification| BRead[Fast O(log N) Single Disk Seek]
    BTree -->|High Write Amplification| BWrite[Random Disk Writes Thrash I/O]
  end

subgraph LSM-Tree Storage Model: Write-Optimized
LSM[Append-Only Sequential Writes] -->|Low Write Amplification| LWrite[Fast Sequential RAM/Disk Appends]
LSM -->|High Read Amplification| LRead[Searches MemTable + Multiple SSTable Disk Files]
end

Figure 1: Trade-off comparison between B-Tree in-place updates and LSM-Tree append-only structures.

The Storage Mechanics Definitions

$$\text{Write Amplification Factor (WAF)} = \frac{\text{Bytes Written to Disk}}{\text{Bytes Written by Application}}$$ If an application updates a 50-byte record, and the database writes a full 8 KB disk page ($8,192\text{ bytes}$), $\text{WAF} = \frac{8192}{50} = 163.8$. High WAF degrades SSD lifespan and saturates disk bandwidth.

1. B-Tree Storage Architecture (In-Place Updates)

B-Trees (and their variants like $\text{B}^+\text{Trees}$) organize database files into fixed-size blocks called Pages (typically 4 KB to 16 KB).

flowchart TD
  RootPage[Root Page: Keys 10, 50, 100] --> NodeLeft[Internal Node: Keys 10 to 49]
  RootPage --> NodeRight[Internal Node: Keys 50 to 99]
  
  NodeLeft --> Leaf1[Leaf Page 1: Keys 10..25 + Pointer to Row Data]
  NodeLeft --> Leaf2[Leaf Page 2: Keys 26..49 + Pointer to Row Data]
  
  NodeRight --> Leaf3[Leaf Page 3: Keys 50..75 + Pointer to Row Data]
  NodeRight --> Leaf4[Leaf Page 4: Keys 76..99 + Pointer to Row Data]

Figure 2: Balanced B+Tree page hierarchy linking root, internal, and leaf pages.

How B-Trees Handle Writes and Reads

  1. Reads ($O(\log N)$): The database loads the root page into RAM, traverses internal branch nodes down the tree, and lands on the exact leaf page containing the target key. Point reads require a deterministic number of page seeks ($3 \text{ to } 4$ hops for billions of rows).
  2. Writes (In-Place Page Mutation):
- When a row is updated, the database locates the specific leaf page on disk and overwrites bytes **in-place**. - If a new row is inserted into a full leaf page ($8192\text{ bytes}$ full), the database triggers a **Page Split**: it allocates a new page, moves 50% of the rows to the new page, and updates parent pointer pages. Page splits cause random disk I/O spikes.

The Write-Ahead Log (WAL)

Because writing modified pages directly to disk is slow, B-Tree engines append transaction records to a sequential **Write-Ahead Log (WAL)** on disk first before modifying pages in RAM (Buffer Pool). If power dies, the engine replays the WAL to recover page state.

2. Log-Structured Merge-Tree (LSM-Tree) Architecture

LSM-Trees eliminate random disk writes by converting all mutation operations (INSERT, UPDATE, DELETE) into sequential append-only writes.

flowchart TB
  subgraph Memory Layer
    WAL[Sequential Write-Ahead Log on Disk]
    MemTable[MemTable: Sorted SkipList in RAM]
  end

subgraph Disk Layer
SST0[Level 0 SSTables: Un-Sorted Flushed Runs]
SST1[Level 1 SSTables: Key-Sorted Compacted Runs]
SST2[Level 2 SSTables: Larger Compacted Runs]
end

AppWrite[Application Write] --> WAL
AppWrite --> MemTable
MemTable -->|MemTable Full: Flush to Disk| SST0
SST0 -->|Background Compaction| SST1
SST1 -->|Background Compaction| SST2

Figure 3: Multi-tiered LSM-Tree architecture moving data from RAM MemTables to Level 0..N SSTables.

The 4 Pillars of LSM-Tree Storage

  1. MemTable (RAM):
An in-memory sorted data structure (usually a Concurrent SkipList or Red-Black Tree). Incoming writes are inserted into the MemTable in sorted key order ($O(\log N)$ in RAM).
  1. Write-Ahead Log (WAL on Disk):
A sequential disk log file. Incoming writes are appended to the WAL to ensure crash recovery durability before returning success to the application.
  1. Sorted String Tables (SSTables on Disk):
When the MemTable reaches its capacity limit (e.g. 64 MB), it is frozen, converted to an immutable **SSTable file**, and written sequentially to disk. SSTables contain sorted `(key, value)` pairs and are **100% immutable** (never overwritten in-place).
  1. Deletes (Tombstones):
Because SSTables are immutable, deleting a row does not erase disk bytes. Instead, the engine appends a special marker called a **Tombstone**. The record is physically deleted later during background compaction.

LSM-Tree Compaction: Leveled vs. Size-Tiered

Over time, flushing MemTables creates hundreds of immutable SSTable files on disk. To prevent read performance from degrading, the LSM-Tree engine merges SSTables using Compaction:

flowchart TD
  subgraph Size-Tiered Compaction
    ST1[SSTable 1: 64MB] & ST2[SSTable 2: 64MB] & ST3[SSTable 3: 64MB] & ST4[SSTable 4: 64MB]
    ST1 & ST2 & ST3 & ST4 -->|Merge-Sort| STBig[Single 256MB SSTable]
  end

subgraph Leveled Compaction
L0[L0: Overlapping Keys] -->|Promote & Merge-Sort| L1[L1: Non-Overlapping Key Ranges]
L1 -->|Promote & Merge-Sort| L2[L2: 10x Larger Key Ranges]
end

Figure 4: Comparing Size-Tiered Compaction against Leveled Compaction.

1. Size-Tiered Compaction

2. Leveled Compaction (RocksDB / LevelDB Standard)


Architectural Comparison: B-Tree vs. LSM-Tree

Property / VectorB-Tree ArchitectureLSM-Tree Architecture
Write ModelIn-place page overwrites on disk ($8\text{ KB}$ pages).Sequential append-only to MemTable & WAL.
Write PerformanceModerate (Random disk writes + page splits).Ultra-High (Sequential RAM inserts).
Read PerformanceUltra-Fast ($O(\log N)$ single page seek).Variable (Searches MemTable + Bloom Filters + SSTables).
Write Amplification (WAF)High (Overwrites full pages for small edits).Low to Moderate (Buffered sequential flushes).
Read Amplification (RAF)Low (Deterministic tree depth).High (May check multiple SSTable files across levels).
Space AmplificationModerate (Page fragmentation + free space holes).Low (SSTables are densely packed and compressed).
Deletes / UpdatesOverwrites or frees page slots in-place.Appends new versions or Tombstones.
Concurrency ModelComplex page latches and row locks.Lock-free MemTable skips + immutable disk files.
Production ExamplesPostgreSQL, MySQL InnoDB, SQLite, Oracle.RocksDB, Apache Cassandra, LevelDB, ScyllaDB.

Complete Worked Example: Go LSM-Tree MemTable & SSTable Flush Engine

Let's inspect the complete Go implementation of an in-memory MemTable that flushes sorted key-value pairs to disk as an immutable SSTable for the DataLab engine (datalab.com).

package main

import (
"bytes"
"encoding/binary"
"fmt"
"os"
"sort"
"sync"
)

type DataEntry struct {
Key string
Value string
}

type MemTable struct {
mu sync.RWMutex
data map[string]string
sizeBytes int
maxSize int
}

func NewMemTable(maxSizeBytes int) *MemTable {
return &MemTable{
data: make(map[string]string),
maxSize: maxSizeBytes,
}
}

func (m *MemTable) Put(key, value string) bool {
m.mu.Lock()
defer m.mu.Unlock()

m.data[key] = value
m.sizeBytes += len(key) + len(value)

// Return true if MemTable exceeds capacity limit and requires disk flush
return m.sizeBytes >= m.maxSize
}

// FlushMemTableToSSTable writes sorted entries sequentially to disk
func (m *MemTable) FlushMemTableToSSTable(filename string) error {
m.mu.Lock()
defer m.mu.Unlock()

// 1. Sort keys alphabetically (Mandatory for SSTables!)
keys := make([]string, 0, len(m.data))
for k := range m.data {
keys = append(keys, k)
}
sort.Strings(keys)

// 2. Open binary file for sequential writing
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()

var buf bytes.Buffer
for _, k := range keys {
v := m.data[k]
// Write Key Length (4 bytes), Value Length (4 bytes), Key Bytes, Value Bytes
binary.Write(&buf, binary.BigEndian, uint32(len(k)))
binary.Write(&buf, binary.BigEndian, uint32(len(v)))
buf.WriteString(k)
buf.WriteString(v)
}

_, err = file.Write(buf.Bytes())
if err == nil {
fmt.Printf("[SSTABLE FLUSH SUCCESS] Flushed %d keys to %s\n", len(keys), filename)
m.data = make(map[string]string)
m.sizeBytes = 0
}
return err
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection MetricMitigation Strategy
1. LSM Compaction StallIngestion write rate exceeds background SSTable compaction speed.Write latency spikes from 1ms to 5,000ms as engine halts MemTable flushes.Spikes in compaction_backlog_bytes and write stall alerts.Throttle incoming write RPS at API gateway; allocate dedicated SSD I/O bandwidth for compaction workers.
2. B-Tree Page Split ThrashingRandom Primary Key UUIDs inserted into a B-Tree table causing continuous page splits.Severe disk write amplification; database buffer pool RAM cache miss rate spikes.High disk write throughput alongside low application insert QPS.Use monotonically increasing primary keys (UUIDv7, ULID, AUTO_INCREMENT) to append to right-most pages.
3. Tombstone Read AmplificationApplication executes millions of row deletes, accumulating un-compacted Tombstones in SSTables.SELECT queries scan millions of deleted tombstone markers, taking seconds to return.High tombstones_scanned_per_query metrics.Trigger aggressive manual SSTable compaction or adjust max tombstone deletion thresholds.
4. Un-Bounded Space AmplificationSize-Tiered compaction requires 50% free disk space to merge large SSTables; disk fills to 95%.Compaction fails due to Out of Disk Space errors; database locks into read-only mode.High disk utilization percentage dashboards ($> 80\%$).Maintain $> 30\%$ free disk space headroom or migrate to Leveled Compaction.

What You Should Remember

  1. B-Trees optimize for reads: B-Trees use balanced in-place page structures to deliver fast, deterministic $O(\log N)$ point reads.
  2. LSM-Trees optimize for writes: LSM-Trees convert random writes into sequential appends in RAM (MemTables) before flushing immutable SSTables to disk.
  3. MemTables use WALs for crash durability: MemTables store sorted writes in RAM for speed; WALs log writes to disk for recovery.
  4. Compactions clean up SSTables and Tombstones: Background compactions merge immutable SSTables and purge deleted Tombstone markers.
  5. Use UUIDv7 to prevent B-Tree page splits: Sequential primary keys prevent destructive B-Tree page split thrashing.

Glossary of Terms

TermDefinition
B-TreeA self-balancing search tree database index that updates fixed-size disk pages in-place.
LSM-TreeLog-Structured Merge-Tree; a write-optimized storage architecture using RAM MemTables and disk SSTables.
MemTableAn in-memory sorted data structure that buffers incoming LSM-Tree writes before flushing to disk.
SSTable (Sorted String Table)An immutable disk file containing sorted (key, value) pairs flushed from a MemTable.
Write Amplification (WAF)The ratio of physical disk bytes written relative to logical application write bytes.
TombstoneA marker appended to an LSM-Tree to indicate that a specific key has been deleted.
CompactionA background process that merges overlapping SSTables, removes duplicate keys, and purges Tombstones.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing the storage engine selection for an analytics platform (`analyticslab.com`):
  1. User Auth Store (10,000 read queries/sec, primary key lookups, strict low read latency)
  2. Clickstream Event Log (500,000 append-only event writes/sec, continuous ingestion)
**Questions**:
  1. Recommend whether a B-Tree or LSM-Tree engine is appropriate for each of the two workloads and justify your reasoning.
  2. Explain why inserting random UUIDv4 primary keys causes high Write Amplification in B-Trees but not in LSM-Trees.

Interactive Self-Assessment

LSM-Trees convert random writes into fast sequential RAM and disk append operations.

LSM-Trees store all historical data permanently in RAM without writing to disk.

LSM-Trees bypass database index structures completely.

LSM-Trees do not use operating system file systems.

It marks a key as deleted in memory/disk without modifying existing immutable SSTable files immediately.

It encrypts old SSTable files to prevent unauthorized data access.

It signals the API gateway to disconnect client TCP sockets.

It resets the primary key AUTO_INCREMENT counter to zero.


What to Learn Next

Track: Data, Storage and Messaging

Previous: Consistent Hashing — Rings, Virtual Nodes, and Minimal Rebalancing

Next: Database Indexes — Find Rows Without Scanning Everything

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab