Chapter 9: Storage Engines

B-Trees vs LSM-Trees, Write Amplification, Compaction

1. What is a Storage Engine?

A storage engine is the component of a database that manages how data is stored, retrieved, and updated on disk. It sits below the query layer and is responsible for physical data organization, indexing, concurrency control, and crash recovery.

graph TD A[SQL Query] --> B[Query Parser] B --> C[Query Optimizer] C --> D[Execution Engine] D --> E[Storage Engine] E --> F[Buffer Pool/Cache] E --> G[WAL] E --> H[Data Files on Disk] E --> I[Index Files] style E fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style F fill:#4ecdc4,color:#1a1a2e,stroke:#0d9488 style H fill:#95e1d3,color:#1a1a2e,stroke:#0d9488
Pluggable Storage Engines
MySQL supports multiple storage engines (InnoDB, MyISAM, Memory). MongoDB switched from MMAPv1 to WiredTiger. PostgreSQL has pluggable table access methods (heap, zheap). This allows you to choose the right tradeoffs per workload.

Storage Engine Responsibilities

Responsibility Description Example Mechanisms
Data Layout How rows/documents are physically organized B-Tree pages, LSM SSTables, heap files
Indexing Fast lookup structures for queries B-Tree indexes, hash indexes, bloom filters
Concurrency Control Multi-version concurrency control (MVCC) Row versioning, timestamps, locks
Crash Recovery Durability guarantees (ACID) Write-Ahead Log (WAL), checkpoints
Caching In-memory buffering to reduce disk I/O Buffer pool (InnoDB), block cache (RocksDB)
Compression Reduce storage footprint Page compression, prefix compression
Interview Question: "Why does MySQL allow different storage engines?"
Answer: Different workloads have different needs. InnoDB is optimized for OLTP (transactional, row-level locking, ACID). MyISAM was historically faster for read-heavy workloads with table-level locking. Memory engine is for temporary tables. Pluggable engines let you optimize per table.

2. B-Tree Deep Dive

B-Trees (Balanced Trees) are the workhorse of traditional databases. They provide O(log n) reads and writes, efficient range scans, and in-place updates.

Structure & Properties

  • Balanced: All leaf nodes are at the same depth
  • Branching Factor: Each node has up to m children (typically 100-200 for disk-based trees)
  • Sorted: Keys are kept in sorted order within nodes
  • Self-Balancing: Automatically rebalances on insert/delete via splits and merges
graph TD A["Root: [50]"] --> B["Internal: [20, 35]"] A --> C["Internal: [70, 85]"] B --> D["Leaf: [10, 15]"] B --> E["Leaf: [25, 30]"] B --> F["Leaf: [40, 45]"] C --> G["Leaf: [55, 60]"] C --> H["Leaf: [75, 80]"] C --> I["Leaf: [90, 95]"] style A fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style B fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style C fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style D fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style E fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style F fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style G fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style H fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style I fill:#6bcf7f,color:#1a1a2e,stroke:#37a850

B-Tree vs B+ Tree

Feature B-Tree B+ Tree
Data Location Internal nodes AND leaves store data Only leaf nodes store data
Leaf Linking No links between leaves Leaves linked as doubly-linked list
Range Scans Requires tree traversal Sequential scan of leaf nodes
Internal Node Size Smaller fanout (stores data) Higher fanout (only keys + pointers)
Used By File systems (NTFS, HFS+) Most databases (InnoDB, PostgreSQL)
Why B+ Trees Dominate Databases:
  1. Range Scans: Linked leaf nodes enable fast sequential reads (SELECT * WHERE age BETWEEN 25 AND 35)
  2. Higher Fanout: Internal nodes are smaller, so more fit in memory → fewer disk seeks
  3. Predictable Performance: All data at same depth → consistent latency

Operations

Search - O(log n)

sequenceDiagram participant Client participant Root participant Internal participant Leaf Client->>Root: Search key=42 Root-->>Root: Binary search in node Root->>Internal: Follow pointer (key < 50) Internal-->>Internal: Binary search in node Internal->>Leaf: Follow pointer (40 < key < 50) Leaf-->>Leaf: Binary search in node Leaf-->>Client: Return value for key=42

Insertion - Page Splits

When a node overflows (exceeds max keys), it splits into two nodes. This can cascade up the tree.

graph TD A["Before Insert: [10, 20, 30, 40]
Insert 25"] --> B["Node Full! Split at median (25)"] B --> C["Left: [10, 20]"] B --> D["Right: [30, 40]"] B --> E["Promote 25 to parent"] style A fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style B fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style C fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style D fill:#6bcf7f,color:#1a1a2e,stroke:#37a850

Page Size Tradeoffs

Page Size Pros Cons Use Case
4 KB Matches OS page, less write amplification Lower fanout, more tree height OLTP, small rows
8 KB PostgreSQL default, good balance Middle ground General purpose
16 KB InnoDB default, higher fanout, fewer seeks More write amplification, wasted space for small rows OLTP with large rows
32-64 KB Very high fanout, shallow tree High write amplification, SSD inefficiency Analytical workloads
Interview Question: "Why is B-Tree height typically 3-4 even for billions of rows?"
Answer: High fanout. With 16KB pages, 100-byte rows, and 8-byte pointers, each internal node can have ~160 children. A 4-level tree can store 160^3 ≈ 4 million leaf pages × 100 rows/page = 400 million rows. With compression and larger pages, billions of rows fit in 4 levels.

Write Amplification in B-Trees

A 100-byte write can trigger:

  1. Read: Load 16KB page from disk
  2. Modify: Update 100 bytes in memory
  3. Write: Write entire 16KB page back (160x amplification)
  4. Cascading Splits: If page splits, write 2 pages + update parent (up to 48KB for single insert)
  5. WAL: Write log entry (~200 bytes)

Total: 10-30x write amplification is typical.

3. LSM-Tree Deep Dive

Log-Structured Merge Trees optimize for write throughput by batching writes in memory and sequentially flushing to disk. Reads are slower but mitigated with caching and bloom filters.

Architecture

graph TD A[Write] --> B[Memtable - In-Memory Sorted Tree] B --> C[Write-Ahead Log - WAL] B --> |Flush when full| D[SSTable L0 - Immutable Sorted File] D --> E[SSTable L1] D --> F[SSTable L1] E --> |Compaction| G[SSTable L2] F --> |Compaction| G G --> H[SSTable L3 ... L6] I[Read] --> B I --> J[Bloom Filter Check] J --> |Maybe exists| D J --> |Maybe exists| E J --> |Maybe exists| G style B fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style C fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style D fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style E fill:#4ecdc4,color:#1a1a2e,stroke:#0d9488 style G fill:#95e1d3,color:#1a1a2e,stroke:#0d9488

Write Path - Sequential Writes Win

  1. Write to WAL: Append-only log for crash recovery (~200 bytes, <1ms)
  2. Insert into Memtable: In-memory skip list or AVL tree (O(log n), microseconds)
  3. Flush to SSTable: When memtable reaches size threshold (e.g., 64MB), write sorted run to L0 (sequential disk write, ~100 MB/s on SSD)
  4. Background Compaction: Merge overlapping SSTables across levels to maintain sorted order
Why LSM is Fast for Writes:
Sequential disk writes are 10-100x faster than random writes on SSDs (500 MB/s vs 50 MB/s for random). LSM converts random writes into sequential ones via batching.

Read Path - Multi-Level Lookup

sequenceDiagram participant Client participant Memtable participant BloomFilter participant L0 participant L1 participant L2 Client->>Memtable: Read key=42 Memtable-->>Client: Not found Client->>BloomFilter: Check L0 SSTables BloomFilter-->>Client: SSTable-3 may contain key Client->>L0: Read SSTable-3 L0-->>Client: Not found Client->>BloomFilter: Check L1 SSTables BloomFilter-->>Client: SSTable-10 may contain key Client->>L1: Read SSTable-10 L1-->>Client: Found! Return value
  1. Check Memtable: Most recent writes (O(log n))
  2. Check Immutable Memtable: Being flushed to disk (if exists)
  3. Check L0 SSTables: May overlap, need to check all (use bloom filters)
  4. Check L1...Ln SSTables: Non-overlapping per level, binary search by key range
  5. Merge: If key exists in multiple levels (e.g., updates), return most recent version

SSTable Structure

Component Purpose Size Example
Data Block Sorted key-value pairs, compressed 64 KB per block
Index Block Sparse index: first key of each data block ~1% of file size
Bloom Filter Probabilistic test: "key definitely NOT here" or "maybe here" 10 bits/key ≈ 1% false positive
Footer Metadata: compression, block offsets ~100 bytes

Levels & Compaction

Level Size Limit Overlapping Keys? Files
L0 10-20 files Yes (each flush creates new file) 10-20 SSTables
L1 10 MB (RocksDB default) No (sorted runs) ~10 files × 1 MB
L2 100 MB (10x L1) No ~100 files × 1 MB
L3 1 GB No ~1000 files × 1 MB
L4 10 GB No ~10000 files × 1 MB
L5 100 GB No ~100000 files × 1 MB
L6 1 TB+ No ~1M files × 1 MB
RocksDB Leveled Compaction: When Li exceeds size threshold, pick overlapping files from Li and Li+1, merge-sort, write to Li+1. This maintains the invariant: Li+1 is 10x the size of Li.

Bloom Filters - Avoiding Disk Reads

A bloom filter is a probabilistic data structure that can answer "definitely NOT in set" or "maybe in set".

graph LR A[Key: user_42] --> B[Hash1: 7] A --> C[Hash2: 23] A --> D[Hash3: 51] B --> E[Bit Array: 64 bits] C --> E D --> E E --> F[Check: All bits set?
Yes = Maybe exists
No = Definitely not] style A fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style E fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style F fill:#4ecdc4,color:#1a1a2e,stroke:#0d9488
Bits per Key False Positive Rate Overhead (1M keys)
8 bits 2.3% 1 MB
10 bits 0.9% 1.25 MB
12 bits 0.4% 1.5 MB
16 bits 0.02% 2 MB

Impact: With 10 bits/key and 20 SSTables in L0, bloom filters eliminate ~19.8 unnecessary disk reads per lookup (only 0.9% false positives).

4. B-Tree vs LSM-Tree Comparison

Dimension B-Tree LSM-Tree Winner
Write Throughput 10-50K writes/sec (random I/O) 100-500K writes/sec (sequential I/O) LSM (5-10x)
Write Amplification 10-30x (page updates + WAL) 10-50x (compaction overhead) Tie (depends on workload)
Read Latency O(log n) - Single tree lookup O(log n × levels) - Check multiple levels B-Tree (2-5x faster)
Read Amplification 1-2 pages (tree path) 1-10+ SSTables (worst case) B-Tree
Range Scans Excellent (sequential leaf scan) Good (but requires merging across levels) B-Tree
Space Amplification Low (in-place updates, 10-20% overhead) High (multiple versions during compaction, 20-50%) B-Tree
Write Stalls Rare (only during page splits) Common (L0 compaction backlog) B-Tree
Point Lookups Fast (O(log n), ~3-4 disk reads) Slower (bloom filters + multiple SSTables) B-Tree
Updates In-place (fast) Append-only (treated as insert + tombstone) B-Tree
Deletes In-place (fast) Tombstone marker (space not freed until compaction) B-Tree
Concurrency Latch contention on hot pages Lock-free writes to memtable LSM
SSD Wear Moderate (random writes) High (compaction rewrites data) B-Tree

Performance Numbers - Real Hardware

Benchmark B-Tree (InnoDB) LSM (RocksDB) Hardware
Random Writes 20K ops/sec 150K ops/sec NVMe SSD, 16 threads
Random Reads 80K ops/sec 40K ops/sec 50% cache hit rate
Range Scan (1M rows) 2.5 sec 4.1 sec Sequential scan
Bulk Load (100M rows) 45 min 15 min Sorted input

When to Choose B-Tree

  • Read-Heavy: OLTP workloads with 80%+ reads (e.g., web app backends)
  • Point Lookups: SELECT * WHERE id = ? dominates
  • Updates: Frequent in-place updates (UPDATE users SET last_login = NOW())
  • Consistent Latency: No compaction stalls (trading higher p99 for predictable p50)
  • Low Space Overhead: Storage constrained

When to Choose LSM-Tree

  • Write-Heavy: Time-series, logging, analytics ingestion (90%+ writes)
  • High Throughput: Need to sustain 100K+ writes/sec
  • Append-Only: Inserts dominate (no updates/deletes)
  • SSD Optimization: Leverage sequential write performance
  • Compression: Better compression ratios (entire SSTables compressed)
Interview Question: "Cassandra uses LSM, but MongoDB uses B-Tree (WiredTiger). Why?"
Answer: Cassandra targets write-heavy time-series and logging (append-only). MongoDB is general-purpose OLTP with mixed reads/writes and frequent updates — B-Tree's in-place updates and faster reads win. Note: WiredTiger actually uses an LSM option for specific workloads.

5. Write Amplification

Write Amplification (WA) = Total bytes written to disk / Bytes written by application

A 1 KB application write might cause 30 KB of disk writes due to:

  • Updating data structures (pages, SSTables)
  • Write-ahead logging
  • Compaction (LSM) or page splits (B-Tree)

B-Tree Write Amplification

sequenceDiagram participant App participant WAL participant BufferPool participant Disk App->>WAL: 1. Write 100-byte log entry WAL->>Disk: Append to WAL file (100 bytes) App->>BufferPool: 2. Update row in buffer pool BufferPool-->>BufferPool: Modify 100 bytes in 16KB page BufferPool->>Disk: 3. Flush dirty page (16KB) Note over Disk: Total: 100 + 16,384 = 16,484 bytes
WA = 16,484 / 100 ≈ 165x

Factors:

  • Page Size: 16 KB pages → 160x amplification for 100-byte writes
  • Page Splits: Can trigger 2-3 page writes (48 KB)
  • WAL: Additional 100-500 bytes per write
  • Double-Write Buffer: InnoDB writes pages twice (once to double-write buffer, then to data file) → 2x amplification

Typical WA: 10-30x for OLTP workloads

LSM-Tree Write Amplification

sequenceDiagram participant App participant WAL participant Memtable participant L0 participant L1 participant L2 App->>WAL: 1. Append log (100 bytes) App->>Memtable: 2. Insert (100 bytes) Memtable->>L0: 3. Flush (64 MB) L0->>L1: 4. Compact L0→L1 (read 64MB L0 + 640MB L1, write 640MB L1) L1->>L2: 5. Compact L1→L2 (read 10MB L1 + 100MB L2, write 100MB L2) Note over L2: Total written: 100 + 64MB + 640MB + 100MB
≈ 800MB for 64MB of user data
WA ≈ 12.5x

Compaction Overhead:

  • Leveled Compaction (RocksDB): Each level is 10x the previous. A write at L0 gets rewritten ~5 times → WA ≈ 10-30x
  • Size-Tiered Compaction (Cassandra): Merge SSTables of similar size. WA ≈ 5-10x but more space amplification

Calculation: For leveled compaction with 10x growth and 6 levels:

WA = 1 (WAL) + 1 (L0 flush) + 10 (L0→L1) + 10 (L1→L2) + 10 (L2→L3) + ... ≈ 50x worst case

SSD Wear & Endurance

SSD Type TBW (Terabytes Written) Daily Writes @ 100 GB/day Lifespan with WA=30x
Consumer (QLC) 600 TBW (1 TB drive) 3 TB/day (30x amp) 200 days
Pro (TLC) 1,200 TBW 3 TB/day 400 days
Enterprise (SLC/MLC) 10,000 TBW 3 TB/day 3,333 days (~9 years)
Production Lesson: A startup using RocksDB with aggressive compaction (WA=50x) burned through consumer SSDs in 6 months. Switching to enterprise SSDs and tuning compaction (WA=15x) extended lifespan to 5 years.

Minimizing Write Amplification

Technique Storage Engine WA Reduction
Larger Memtables LSM Fewer flushes → fewer compactions (128MB vs 64MB: 2x reduction)
Universal Compaction LSM (RocksDB) Compact all levels at once → WA ≈ 10x (vs 30x leveled)
Disable WAL Both Save 1x amplification (but lose durability!)
Group Commit Both Batch WAL writes → amortize fsync cost
Compression Both Fewer bytes written (e.g., 50% compression → 0.5x multiplier)
Disable Double-Write B-Tree (InnoDB) Save 2x amplification (but risk partial page writes)
Interview Question: "How does RocksDB achieve 10x lower write amplification than early LSM implementations?"
Answer: (1) Leveled compaction instead of size-tiered, (2) Bloom filters reduce unnecessary compactions, (3) Compression at SSTable level, (4) Dynamic level sizing (levels grow geometrically), (5) Subcompaction (parallelize compaction across key ranges).

6. Compaction Strategies

Compaction merges SSTables to remove duplicate keys, apply tombstones, and maintain sorted order. The strategy trades off write amplification, space amplification, and read amplification.

Size-Tiered Compaction (STCS)

Used by: Cassandra (default), HBase

graph LR A[New Writes] --> B[SST 10MB] B --> C[SST 10MB] C --> D[SST 10MB] D --> E[SST 10MB] E --> F[Compact: Merge 4 SSTables] F --> G[SST 40MB] G --> H[SST 40MB] H --> I[Compact: Merge 4 SSTables] I --> J[SST 160MB] style F fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style I fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a

Algorithm:

  1. Group SSTables of similar size (e.g., 10 MB)
  2. When 4 SSTables accumulate, merge them into one (40 MB)
  3. Repeat at next tier (4 × 40 MB → 160 MB)

Pros:

  • Low write amplification (each SSTable compacted ~log4(N) times → WA ≈ 5-10x)
  • Simple to implement

Cons:

  • High space amplification (multiple overlapping SSTables → 2-3x overhead)
  • Slow reads (must check many SSTables)
  • Temporary space spike during compaction (need 2x space)

Leveled Compaction (LCS)

Used by: RocksDB (default), LevelDB, Cassandra (optional)

graph TD A[L0: 4 overlapping SSTables] --> B[Compact L0 + L1] B --> C[L1: 10 non-overlapping SSTables] C --> D[Compact L1 + L2] D --> E[L2: 100 non-overlapping SSTables] E --> F[Compact L2 + L3] F --> G[L3: 1000 non-overlapping SSTables] style B fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style D fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style F fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a

Algorithm:

  1. L0 allows overlapping SSTables (direct memtable flushes)
  2. L1...Ln have non-overlapping SSTables within each level
  3. Li+1 is 10x the size of Li
  4. When Li exceeds threshold, pick overlapping SSTables from Li and Li+1, merge, write to Li+1

Pros:

  • Fast point lookups (bloom filters + at most 1 SSTable per level → check ~7 SSTables)
  • Low space amplification (~10% overhead, no overlapping SSTables except L0)
  • Predictable performance (no temporary space spikes)

Cons:

  • High write amplification (each byte rewritten ~10x per level → WA ≈ 30x)
  • Compaction can bottleneck writes (L0 stalls)

Universal Compaction

Used by: RocksDB (option for write-heavy workloads)

Algorithm: Hybrid between size-tiered and leveled. Pick SSTables to compact based on size ratio and age.

Pros:

  • Lower write amplification than leveled (WA ≈ 10-15x)
  • Good for write-heavy workloads

Cons:

  • Higher space amplification than leveled
  • More complex tuning

FIFO Compaction

Used by: RocksDB (time-series data)

Algorithm: Drop oldest SSTables when total size exceeds threshold. No merging.

Pros:

  • Minimal write amplification (WA ≈ 1x, just WAL + flush)
  • Perfect for TTL-based data (logs, metrics)

Cons:

  • No duplicate removal (wastes space if keys updated)
  • Requires time-ordered keys

Compaction Strategy Comparison

Strategy Write Amp Space Amp Read Amp Use Case
Size-Tiered Low (5-10x) High (200-300%) High (10-20 SSTables) Write-heavy, storage abundant
Leveled High (20-40x) Low (110-120%) Low (1-2 SSTables/level) Read-heavy, point lookups
Universal Medium (10-20x) Medium (150-200%) Medium (5-10 SSTables) Balanced workloads
FIFO Minimal (1-2x) Low (110%) High (all SSTables) Time-series, TTL data
RocksDB Tuning Example:
For time-series metrics (write-heavy, TTL-based), switch from leveled to FIFO compaction. This reduced write amplification from 30x to 2x, increasing write throughput from 50K ops/sec to 400K ops/sec.

Compaction Triggers & Throttling

Compaction runs in background threads but can stall writes if it falls behind.

Condition Action Impact
L0 reaches soft limit (e.g., 20 files) Slow down writes (sleep 1ms per write) Write throughput drops 50%
L0 reaches hard limit (e.g., 40 files) Stall writes until compaction catches up Write latency spikes to seconds
Memtable full Flush to L0 Triggers L0→L1 compaction
Disk space low Aggressive compaction or reject writes Availability risk
Interview Question: "Why does Cassandra use size-tiered compaction instead of leveled?"
Answer: Cassandra targets write-heavy workloads (time-series, logging). Size-tiered has 5-10x write amplification vs 30x for leveled. Cassandra accepts higher space amplification (200-300%) and slower reads in exchange for 3x higher write throughput. For read-heavy tables, Cassandra supports leveled compaction as an option.

7. Write-Ahead Log (WAL)

The Write-Ahead Log ensures durability by logging all changes before applying them to the database. If the database crashes, it replays the WAL to recover uncommitted changes.

Why WAL is Needed

sequenceDiagram participant Client participant DB participant BufferPool participant Disk Client->>DB: UPDATE users SET balance=100 DB->>BufferPool: Modify page in memory Note over DB,BufferPool: Page is "dirty" (not yet on disk) DB-->>Client: ✓ Committed Note over Disk: CRASH! Dirty page lost! Note over Disk: Without WAL: Data loss
With WAL: Replay log entry

Problem: Databases use in-memory buffer pools to cache pages. If a transaction commits but the dirty page hasn't been flushed to disk, a crash loses the change.

Solution: Write to WAL first (cheap sequential append), then modify buffer pool. On crash, replay WAL.

WAL Structure

Field Size Description
LSN 8 bytes Log Sequence Number (monotonically increasing)
Transaction ID 8 bytes Which transaction made this change
Record Type 1 byte INSERT, UPDATE, DELETE, COMMIT, ABORT
Table ID 4 bytes Which table was modified
Page ID 4 bytes Which page was modified
Offset 2 bytes Offset within page
Before Image Variable Old value (for UNDO)
After Image Variable New value (for REDO)
Checksum 4 bytes Detect corruption

Total: ~100-500 bytes per log entry (depending on data size)

WAL Write Process

sequenceDiagram participant Client participant DB participant WALBuffer participant Disk participant BufferPool DB->>WALBuffer: 1. Append log entries to WAL buffer DB->>Disk: 2. fsync() WAL to disk DB-->>Client: 3. ✓ Transaction committed DB->>BufferPool: 4. Modify pages in buffer pool (async) BufferPool->>Disk: 5. Flush dirty pages later (async)

Key Insight: WAL writes are sequential (fast), data page writes are random (slow). WAL makes commits fast by deferring expensive random I/O.

Group Commit Optimization

fsync() is expensive (~5-10ms on SSD). Batching multiple transactions into one fsync improves throughput.

gantt title Group Commit Timeline dateFormat X axisFormat %L ms section Txn 1 Write WAL :0, 1 Wait :1, 5 fsync :5, 10 section Txn 2 Write WAL :2, 3 Wait :3, 10 fsync :10, 10 section Txn 3 Write WAL :4, 5 Wait :5, 10 fsync :10, 10

Transactions 1, 2, 3 all commit with a single fsync → 3x throughput improvement.

Checkpoint & WAL Truncation

WAL grows unbounded. Periodically, the database performs a checkpoint:

  1. Flush all dirty pages to disk
  2. Record checkpoint LSN in a special file
  3. Truncate WAL entries before checkpoint LSN (they're now redundant)

Recovery: Start from last checkpoint, replay WAL from checkpoint LSN to end.

WAL & Replication

WAL is also used for replication:

  • PostgreSQL: Replicas stream WAL entries from primary
  • MySQL: Binary log (similar to WAL) is shipped to replicas
  • RocksDB: WAL can be tailed for change data capture (CDC)
PostgreSQL WAL Archiving: For point-in-time recovery (PITR), PostgreSQL can archive WAL segments to S3. To restore, load a base backup + replay archived WAL up to desired timestamp.

WAL Performance Tuning

Parameter Default Tuning Impact
fsync Mode fsync fdatasync (skip metadata sync) 10-20% faster commits
WAL Buffer Size 16 MB 64 MB (high write volume) Reduce fsync frequency
Commit Delay 0 ms 10 ms (batch commits) 5-10x throughput (higher latency)
Synchronous Commit On Off (async replication) 2-3x throughput (risk data loss)
Warning: Disabling fsync or synchronous commit risks data loss on crash. Only do this for non-critical data (e.g., caches, analytics staging).
Interview Question: "Why doesn't the database just write pages directly to disk instead of using a WAL?"
Answer: (1) Sequential WAL writes are 10-100x faster than random page writes. (2) Transactions can modify multiple pages; WAL ensures atomicity (either all committed or none). (3) WAL enables group commit (batch fsync for multiple transactions). (4) WAL supports replication and PITR.

8. Page Cache & Buffer Pool

Disk I/O is 1000x slower than memory. Databases cache frequently accessed pages in memory to avoid disk reads.

InnoDB Buffer Pool

InnoDB's buffer pool is a large memory area (typically 50-80% of RAM) that caches data and index pages.

graph TD A[Query: SELECT * FROM users WHERE id=42] --> B{Buffer Pool} B -->|Cache Hit| C[Return row from memory] B -->|Cache Miss| D[Read page from disk] D --> E[Evict LRU page if buffer full] E --> F[Load page into buffer pool] F --> G[Return row from memory] style B fill:#4ecdc4,color:#1a1a2e,stroke:#0d9488 style C fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style D fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a

Buffer Pool Structure

Component Purpose Size
Data Pages Cached table rows ~95% of buffer pool
Index Pages Cached B-Tree index nodes Included in 95%
Insert Buffer Batch secondary index updates ~1% of buffer pool
Adaptive Hash Index In-memory hash index for hot pages ~1% of buffer pool
Lock Info Row locks, table locks ~1% of buffer pool
Data Dictionary Table metadata ~1% of buffer pool

LRU Eviction - Midpoint Insertion

Standard LRU has a problem: a full table scan evicts the entire cache. InnoDB uses midpoint insertion:

graph LR A[LRU List] --> B[Young Sublist - 5/8] A --> C[Old Sublist - 3/8] D[New Page Read] --> E[Insert at midpoint] E --> C F[Page Accessed Again] --> G[Promote to young] G --> B H[Eviction] --> I[Evict from tail of old sublist] I --> C style B fill:#6bcf7f,color:#1a1a2e,stroke:#37a850 style C fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style E fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a

Algorithm:

  1. New pages inserted at midpoint (head of "old" sublist)
  2. If page accessed again within 1 second, promote to "young" sublist
  3. Evict from tail of "old" sublist

Benefit: Full table scan pages stay in "old" sublist and are quickly evicted, preserving hot pages in "young" sublist.

Dirty Page Flushing

Modified pages (dirty pages) must eventually be written to disk. InnoDB has multiple flushing mechanisms:

Mechanism Trigger Urgency
Adaptive Flushing Continuously flush to keep dirty page ratio low (e.g., <75%) Low (background)
Checkpoint Every N seconds (e.g., 60s), flush all dirty pages older than checkpoint age Medium
LRU Flushing When evicting a dirty page, flush it first Medium
Forced Flushing Dirty page ratio exceeds threshold (e.g., 90%) High (can stall writes)

RocksDB Block Cache

RocksDB uses a simpler block cache (similar to OS page cache):

  • Uncompressed Block Cache: Stores decompressed data blocks (fast reads)
  • Compressed Block Cache: Stores compressed blocks (save memory)
  • Index & Filter Cache: Separate cache for index blocks and bloom filters

Eviction: Standard LRU with sharding (16-64 shards to reduce lock contention).

Cache Hit Rate & Performance

Cache Hit Rate Disk Reads/sec (100K queries/sec) Avg Latency
99% 1,000 ~1 ms
95% 5,000 ~3 ms
90% 10,000 ~5 ms
80% 20,000 ~10 ms
50% 50,000 ~30 ms

Rule of Thumb: Every 1% increase in cache hit rate reduces latency by ~10%.

Production Tip: Monitor cache hit rate closely. If <95%, consider:
  • Increasing buffer pool size
  • Adding read replicas to distribute load
  • Partitioning data to reduce working set
  • Adding application-level caching (Redis, Memcached)

Buffer Pool Tuning

Parameter Default Recommended Impact
innodb_buffer_pool_size 128 MB 50-80% of RAM Primary tuning knob
innodb_buffer_pool_instances 1 (if <1GB) 8-16 (reduce contention) Improve concurrency
innodb_old_blocks_pct 37% (3/8) 37% (keep default) Midpoint insertion ratio
innodb_old_blocks_time 1000 ms 1000-5000 ms Delay before promoting to young
innodb_max_dirty_pages_pct 90% 75% (reduce write stalls) Flush threshold
Interview Question: "Why does InnoDB use midpoint insertion instead of standard LRU?"
Answer: Standard LRU has the "sequential scan problem": a full table scan reads millions of pages, evicting the entire cache. Midpoint insertion protects hot pages by inserting new pages in the "old" sublist. If they're accessed again (hot), promote to "young". If not (sequential scan), quickly evict. This keeps hot pages in cache despite large scans.

9. Compression

Compression reduces storage costs and can improve I/O throughput (fewer bytes to read/write). The tradeoff is CPU overhead.

Compression Levels

Level Description Granularity
Page-Level Compress individual B-Tree pages 16 KB pages
Block-Level Compress SSTable data blocks (LSM) 4-64 KB blocks
Column-Level Compress individual columns (columnar stores) Per-column
Dictionary Replace repeated values with short codes Per-column or per-SSTable
Prefix Store common prefixes once (keys in B-Tree) Per-node

Compression Algorithms

Algorithm Compression Ratio Speed (MB/s) CPU Cost Use Case
LZ4 2-3x Compress: 500
Decompress: 3000
Low Default for RocksDB, fast decompression
Snappy 2-3x Compress: 300
Decompress: 1000
Low Cassandra, Parquet, BigTable
Zstd 3-5x Compress: 200
Decompress: 800
Medium Best ratio, Facebook default
Zlib (gzip) 3-5x Compress: 50
Decompress: 300
High Legacy, avoid for hot path
LZMA 5-10x Compress: 10
Decompress: 100
Very High Cold storage, archival
LZ4 Dominates Databases: LZ4's 3000 MB/s decompression speed means it's faster to read compressed data from SSD (500 MB/s) and decompress in CPU than to read uncompressed data. Write bandwidth also improves 2x.

InnoDB Transparent Page Compression

InnoDB compresses individual 16 KB pages:

  1. Compress page (e.g., 16 KB → 8 KB with zlib)
  2. Write compressed page to disk (saves 8 KB)
  3. On read, decompress page into buffer pool (16 KB in memory)

Tradeoff: 30-50% space savings, but 20-30% CPU overhead. Enable for cold tables only.

LSM-Tree Compression

RocksDB compresses entire data blocks (not individual keys):

graph TD A[Data Block: 64 KB
100 key-value pairs] --> B[Compress with LZ4] B --> C[Compressed Block: 22 KB
3x compression] C --> D[Write to SSTable] E[Read Request] --> F[Read 22 KB from disk] F --> G[Decompress to 64 KB] G --> H[Binary search for key] style A fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style C fill:#6bcf7f,color:#1a1a2e,stroke:#37a850

Compression per Level: RocksDB allows different compression per level:

Level Compression Reason
L0-L1 None Hot data, frequent compaction → skip CPU cost
L2-L4 LZ4 Warm data, balance CPU vs space
L5-L6 Zstd Cold data, maximize compression (infrequent access)

Dictionary Compression (LSM)

For repeated strings (e.g., URLs, user agents), RocksDB can build a dictionary per SSTable:

  • Training: Analyze sample keys/values, extract common substrings
  • Encoding: Replace substrings with 2-byte codes
  • Compression Boost: 5-10x for text-heavy data (vs 2-3x with LZ4 alone)

Prefix Compression (B-Tree)

B-Tree nodes store sorted keys. Consecutive keys often share prefixes:

graph TD A["Before:
user_12345
user_12346
user_12347"] --> B["After:
user_1234 (prefix)
+ 5
+ 6
+ 7"] style A fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style B fill:#6bcf7f,color:#1a1a2e,stroke:#37a850

Savings: Store "user_1234" once, then only store suffixes ("5", "6", "7"). For long keys (e.g., URLs), this saves 80-90% space.

Compression Impact on Performance

Metric No Compression LZ4 Zstd
Storage (100 GB dataset) 100 GB 35 GB (2.9x) 25 GB (4x)
Write Throughput 100K ops/sec 120K ops/sec (+20%) 90K ops/sec (-10%)
Read Throughput 80K ops/sec 100K ops/sec (+25%) 70K ops/sec (-12%)
CPU Usage 30% 45% (+15%) 60% (+30%)
When Compression Improves Performance: If I/O is the bottleneck (SSD at 500 MB/s), compression reduces I/O by 3x, so throughput increases despite CPU cost. If CPU is the bottleneck, compression makes it worse.
Interview Question: "Why does RocksDB use no compression for L0-L1 but Zstd for L5-L6?"
Answer: L0-L1 are frequently compacted (high CPU churn), so skip compression to save CPU. L5-L6 are cold data (rarely compacted, rarely read), so aggressive compression (Zstd) maximizes space savings with minimal performance impact. L2-L4 use LZ4 for balance.

10. Real Storage Engines

InnoDB (MySQL)

  • Type: B+ Tree with clustered primary key
  • Concurrency: MVCC (multi-version concurrency control), row-level locking
  • Durability: WAL (redo log) + double-write buffer
  • Strengths: ACID transactions, fast point lookups, efficient range scans
  • Weaknesses: Write amplification (10-30x), page splits under heavy writes
  • Use Case: General-purpose OLTP (e-commerce, SaaS apps)

RocksDB (Meta/Facebook)

  • Type: LSM-Tree with leveled compaction
  • Concurrency: Lock-free writes, reader-writer locks for compaction
  • Durability: WAL + memtable snapshots
  • Strengths: Extreme write throughput (500K ops/sec), tunable compaction, embeddable (C++ library)
  • Weaknesses: Slower reads than B-Tree, compaction stalls
  • Use Case: Write-heavy workloads (time-series, logging, stream processing)
  • Used By: MySQL (MyRocks storage engine), CockroachDB, TiDB, Kafka Streams

WiredTiger (MongoDB)

  • Type: B-Tree (default) or LSM-Tree (configurable)
  • Concurrency: MVCC, document-level locking
  • Durability: WAL (journal) + checkpoints
  • Strengths: Hybrid approach, excellent compression (Zstd), fast snapshots
  • Weaknesses: Complex tuning, checkpoints can cause latency spikes
  • Use Case: MongoDB workloads (document store, mixed read/write)

LevelDB (Google)

  • Type: LSM-Tree with leveled compaction
  • Concurrency: Single-writer, multiple readers
  • Durability: WAL
  • Strengths: Simple API, fast writes, embeddable
  • Weaknesses: Single-threaded writes, no replication, abandoned by Google (RocksDB is successor)
  • Use Case: Embedded key-value store (Chrome IndexedDB, Bitcoin Core)

BadgerDB (Go)

  • Type: LSM-Tree with value separation
  • Concurrency: Multi-threaded writes via MVCC
  • Durability: WAL (value log) + separate key log
  • Strengths: Stores large values separately (reduces compaction overhead), written in Go (no CGO)
  • Weaknesses: Immature compared to RocksDB, garbage collection overhead
  • Use Case: Go applications needing embedded KV store (IPFS, Dgraph)

Comparison Table

Engine Type Language Write Throughput Read Latency Maturity
InnoDB B-Tree C++ 50K ops/sec 1-3 ms 25+ years
RocksDB LSM C++ 500K ops/sec 3-10 ms 10+ years
WiredTiger B-Tree/LSM C 100K ops/sec 2-5 ms 10+ years
LevelDB LSM C++ 100K ops/sec 5-15 ms 13+ years (unmaintained)
BadgerDB LSM + Value Sep Go 200K ops/sec 5-20 ms 5+ years

Pluggable Engines in Practice

MySQL Storage Engine Options:

  • InnoDB: Default, ACID, row-level locking
  • MyISAM: Table-level locking, fast reads, no transactions (deprecated)
  • Memory: In-memory tables, fast but volatile
  • MyRocks: RocksDB backend, 2-3x better compression, 5-10x write throughput
  • TokuDB: Fractal tree, high compression (acquired by Percona, end-of-life)

Example: Facebook migrated MySQL from InnoDB to MyRocks (RocksDB), reducing storage by 50% and increasing write throughput by 3x.

Choosing an Engine: Start with InnoDB (or PostgreSQL heap). If write-heavy, try RocksDB. If document-oriented, use WiredTiger (MongoDB). If embedded Go, use BadgerDB. Measure before switching!

11. Emerging Approaches

Learned Indexes (ML-Based)

Replace B-Tree nodes with machine learning models that predict the position of a key.

graph LR A[Traditional B-Tree] --> B[Root Node] B --> C[Internal Nodes] C --> D[Leaf Nodes] F --> G[Direct to data page] style A fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style E fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style G fill:#6bcf7f,color:#1a1a2e,stroke:#37a850

Idea: B-Tree is essentially a learned function: given key K, predict its position. Train a neural network to approximate this function.

Pros:

  • Smaller index size (MB vs GB for B-Tree)
  • Fewer cache misses (no pointer chasing)
  • Theoretically faster lookups (O(1) vs O(log n))

Cons:

  • Only works for read-only or append-only data (retraining cost)
  • Requires predictable key distribution
  • Complex implementation, unproven at scale

Status: Research stage (Google's 2018 paper), not production-ready.

Bw-Tree (Lock-Free B-Tree)

Microsoft's lock-free B-Tree for SQL Server Hekaton (in-memory OLTP).

Key Ideas:

  • Delta Updates: Instead of modifying nodes in-place, append delta records
  • Atomic CAS: Use compare-and-swap for lock-free updates
  • Log-Structured: Periodically consolidate deltas into new base pages

Pros:

  • High concurrency (no locks, no latches)
  • Scales to 100+ cores

Cons:

  • Complex implementation (1000s of lines vs B-Tree)
  • Delta chains can cause read amplification

Status: Production in SQL Server Hekaton, not widely adopted elsewhere.

Fractal Trees (Toku Storage Engine)

A variant of B-Tree that buffers writes at internal nodes to reduce random I/O.

graph TD A[Root Node + Buffer] --> B[Internal Node + Buffer] A --> C[Internal Node + Buffer] B --> D[Leaf Node] B --> E[Leaf Node] C --> F[Leaf Node] C --> G[Leaf Node] H[Write: Insert key=42] --> A A --> |Buffer full| B B --> |Buffer full| D style A fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style B fill:#ffd93d,color:#1a1a2e,stroke:#f5b800 style D fill:#6bcf7f,color:#1a1a2e,stroke:#37a850

Key Ideas:

  • Each internal node has a buffer (4-64 MB)
  • Writes accumulate in buffers, flushed to children when full
  • Converts random writes into sequential flushes

Pros:

  • Better write throughput than B-Tree (10x for bulk inserts)
  • Better compression than LSM (no duplicate keys across levels)

Cons:

  • Complex implementation
  • Slower point queries (must check buffers up the tree)

Status: TokuDB (Percona) reached end-of-life in 2021. Not widely adopted.

Append-Only B-Trees (CouchDB, Riak)

Instead of in-place updates, write new versions of pages (copy-on-write).

Pros:

  • MVCC without row versioning
  • Fast snapshots (root pointer = snapshot)
  • Crash recovery via root pointer

Cons:

  • High write amplification (entire root-to-leaf path rewritten)
  • Requires compaction to reclaim space

Status: Used in CouchDB and Riak, niche use case.

Key-Value Separation (WiscKey, BadgerDB)

Store keys in LSM-Tree, but store large values in a separate log.

graph TD A[LSM-Tree: Keys + Value Pointers] --> B[Memtable] B --> C[SSTables - Keys Only] D[Value Log: Append-Only] --> E[Large Values] F[Write: key=user_42, value=10KB] --> B F --> D G[Read: key=user_42] --> C C --> |Pointer: offset 12345| D D --> H[Return 10KB value] style A fill:#ff6b6b,color:#1a1a2e,stroke:#c92a2a style D fill:#6bcf7f,color:#1a1a2e,stroke:#37a850

Pros:

  • Reduces compaction overhead (don't rewrite large values)
  • Lower write amplification for value-heavy workloads

Cons:

  • Extra disk read for values (not in cache)
  • Garbage collection complexity (value log grows unbounded)

Status: BadgerDB (production), WiscKey (research).

Future Trends: Storage engines are converging on hybrid approaches. WiredTiger (B-Tree + LSM option), TiDB (RocksDB + TiKV raft), CockroachDB (Pebble, a RocksDB fork). Expect more specialization: column stores for analytics, row stores for OLTP, hybrid for mixed workloads.

Chapter Summary

Key Takeaways

  • B-Trees: Balanced tree, O(log n) reads/writes, in-place updates. Best for read-heavy OLTP.
  • LSM-Trees: Append-only, sequential writes, background compaction. Best for write-heavy workloads.
  • Write Amplification: 10-30x typical. Critical for SSD endurance.
  • Compaction: Size-tiered (low WA, high space), leveled (low space, high WA), FIFO (minimal WA).
  • WAL: Durability via sequential logging. Group commit for throughput.
  • Caching: Buffer pool with midpoint insertion LRU. 99% cache hit rate is ideal.
  • Compression: LZ4 for hot data (fast), Zstd for cold data (high ratio).
  • Real Engines: InnoDB (B-Tree, OLTP), RocksDB (LSM, write-heavy), WiredTiger (hybrid).

Decision Matrix

Workload Recommended Engine Rationale
OLTP (80%+ reads) B-Tree (InnoDB, PostgreSQL) Fast point lookups, efficient updates
Time-series, Logging (90%+ writes) LSM (RocksDB) + FIFO compaction Sequential writes, minimal WA
Mixed Read/Write LSM (RocksDB) + Leveled compaction Balance throughput and latency
Analytics (Range Scans) B-Tree or Columnar (Parquet) Sequential leaf scans or column pruning
Embedded KV Store RocksDB (C++) or BadgerDB (Go) Mature, high performance

Interview Cheat Sheet

Question Answer
"B-Tree vs LSM?" B-Tree: fast reads, in-place updates. LSM: fast writes, sequential I/O. Choose based on read/write ratio.
"Why WAL?" Durability without expensive random writes. Sequential append + group commit = high throughput.
"Why compaction?" Remove duplicate keys, apply tombstones, maintain sorted order. Tradeoff: write amp vs space amp vs read amp.
"Why bloom filters?" Avoid disk reads for non-existent keys. 10 bits/key → 0.9% false positive → 99.1% disk read savings.
"Why LZ4 over Zstd?" LZ4 decompresses at 3000 MB/s (faster than SSD), so hot data benefits. Zstd for cold data (better ratio).
"Why midpoint insertion LRU?" Protect hot pages from sequential scans. New pages start in "old" sublist, promoted to "young" if re-accessed.
Next Steps: Experiment with RocksDB benchmarks, tune InnoDB buffer pool, read Cassandra compaction docs, profile write amplification in your database. Storage engines are the foundation of scalable systems — master this chapter!

Worked Examples: Storage Engine Math

B-Tree Page Split Cascade: Write Amplification Calculation

A 100-byte insert into a full 16KB page triggers a page split. Calculate the actual I/O cost:

Operation I/O Size Explanation
1. Read full page 16KB read Load page from disk to buffer pool
2. Split page 2 × 16KB write Write left half (8KB data + padding) and right half (8KB data + padding)
3. Update parent pointer 16KB read + 16KB write Parent internal node must add new pointer to right child
Total I/O 1 read (16KB) + 3 writes (48KB) = 64KB Write Amplification: 64KB / 100 bytes = 640x (worst case)

Average Case Analysis:

  • Fill Factor: B-Trees maintain 50-70% page occupancy on average
  • Split Frequency: With 70% fill factor and 16KB pages storing ~115 rows (140 bytes each), a split occurs every ~115 inserts
  • Amortized Cost: 16KB split cost / 115 inserts = 140 bytes per insert = 1.4x average write amplification
  • WAL Overhead: Add ~200 bytes per insert for write-ahead log → total ~1.6x average write amp
Interview Insight: "Why do B-Trees have low average write amplification despite expensive splits?" Answer: Most inserts don't trigger splits. With 70% fill factor, only 1 in 115 inserts causes a split. The worst-case 640x amplification amortizes to ~1.4x average. WAL + page flushes add another 10-20x, totaling ~15-30x real-world write amp.

LSM Write Amplification: Multi-Level Compaction

Calculate write amplification for leveled compaction (RocksDB default):

Leveled Compaction Write Path

Stage Action Bytes Written Write Amplification Factor
Level 0: Flush Memtable (64MB) → L0 SSTable 64MB 1x (initial write)
L0 → L1 Compaction Merge L0 (64MB) + L1 (640MB) → L1 (640MB) 640MB written 10x (read 704MB, write 640MB)
L1 → L2 Compaction Merge L1 (10MB) + L2 (100MB) → L2 (100MB) 100MB written 10x per level
L2 → L3 Compaction Merge L2 (10MB) + L3 (100MB) → L3 (100MB) 100MB written 10x per level
Total (6 levels) Data rewritten at each level ~10x per level × 5 compactions 10-30x total write amplification

Size-Tiered Compaction (Cassandra):

  • Merge Strategy: Combine 4 SSTables of similar size → 1 larger SSTable
  • Tiers: 10MB → 40MB → 160MB → 640MB (log4 growth)
  • Write Amplification: Each SSTable compacted log4(N) times = ~5-10x (lower than leveled)
  • Space Amplification: Up to 2x (multiple overlapping SSTables before compaction)
Interview Trade-off Question: "Why use leveled compaction despite higher write amp?" Answer: Read performance. Leveled compaction guarantees non-overlapping SSTables per level → point lookup checks at most 1 SSTable per level (7 SSTables for 6 levels) vs 20-50 SSTables with size-tiered. For read-heavy workloads, 3x write amp penalty is worth 5x read speedup.

RocksDB Tuning Example: High Write Throughput

Production configuration for write-heavy workload (logging, time-series):

# RocksDB Configuration (C++ API or options file)
# Goal: Maximize write throughput, tolerate higher space/read amplification

# Memtable Configuration
write_buffer_size = 256MB          # Larger memtable → fewer flushes
                                   # Default: 64MB. Tradeoff: more memory, slower recovery
max_write_buffer_number = 4        # 4 memtables before write stall
                                   # Total memory: 256MB × 4 = 1GB per column family

# L0 Compaction Tuning
level0_file_num_compaction_trigger = 8
                                   # Start L0→L1 compaction when 8 SSTables in L0
                                   # Higher value = more write throughput, worse reads
level0_slowdown_writes_trigger = 20
                                   # Slow writes when L0 reaches 20 files
level0_stop_writes_trigger = 36
                                   # Stop writes when L0 reaches 36 files (avoid this!)

# Compaction Threads
max_background_compactions = 4     # 4 parallel compaction threads
                                   # Matches typical 4-core CPU, adjust based on hardware
max_background_flushes = 2         # 2 parallel memtable flush threads

# SSTable Configuration
target_file_size_base = 256MB      # Larger SSTables → fewer files, less compaction overhead
                                   # Default: 64MB. Tradeoff: slower individual compactions
max_bytes_for_level_base = 2GB     # L1 size = 2GB (10x larger than default)
max_bytes_for_level_multiplier = 10
                                   # Each level is 10x the previous (L2=20GB, L3=200GB, ...)

# Compression (reduces write amp via smaller files)
compression = "lz4"                # Fast compression (10 GB/s encode)
                                   # Alternatives: snappy (faster), zstd (better ratio)
bottommost_compression = "zstd"    # Higher compression for oldest level (rarely read)

# Write Ahead Log (WAL)
wal_bytes_per_sync = 1MB           # fsync WAL every 1MB (balance durability vs throughput)
                                   # Default: 0 (fsync every write = slow)

# Bloom Filters (mitigate read amplification from high L0 files)
bloom_bits_per_key = 10            # 10 bits = 0.9% false positive rate
                                   # Memory cost: 10 bits × 1M keys = 1.25MB per SSTable

Impact of Configuration

Setting Before (Default) After (Tuned) Impact
write_buffer_size 64MB 256MB 4x fewer flushes → 4x fewer L0 compactions
target_file_size_base 64MB 256MB 4x fewer SSTables → 4x fewer compaction jobs
Write Throughput 50K ops/sec 200K ops/sec 4x improvement (reduced compaction overhead)
Memory Usage 256MB (4 × 64MB) 1GB (4 × 256MB) 4x increase (acceptable for write-heavy workload)
Read Latency (p99) 5ms 12ms 2.4x slower (more L0 files, tolerable for logging)
Real-World Case Study: A time-series metrics database (write-heavy, 95% inserts) tuned RocksDB from default config. Result: Write throughput increased from 50K ops/sec to 200K ops/sec. Read latency increased from 5ms to 12ms (acceptable since reads are <5% of workload). Total cost savings: avoided scaling to 4x more servers.

Interview Callout: Demonstrating First-Principles Understanding

Interviewer asks: "Why are B-Trees read-optimized and LSM-Trees write-optimized?"

Strong answer (shows calculation from first principles):

"B-Trees do in-place updates: a 100-byte write modifies 1 page (16KB). Write amplification is page_size / write_size = 160x per page write, but most inserts don't cause splits, so amortized write amp is ~1.4x for inserts + 10-20x for WAL/page flushes = 15-30x total. Reads are O(log n) with single tree traversal = 3-4 disk seeks. LSM-Trees append to memtable (O(log n) in-memory), then batch-flush to L0. Every write is eventually rewritten through compaction: L0→L1 (10x), L1→L2 (10x), ... for 6 levels = 10-30x write amp. Reads must check multiple levels: memtable + L0 (10 files) + L1-L6 (1 file each with bloom filter) = up to 17 disk seeks worst case. Trade-off: LSM trades write amp for sequential I/O (500 MB/s) vs random I/O (50 MB/s) = 10x throughput gain. B-Tree trades higher write amp for single-lookup reads."

This demonstrates: (1) Quantitative reasoning, (2) Understanding of I/O characteristics, (3) Knowledge of real-world performance numbers, (4) Ability to articulate trade-offs.