Deep Dive into Storage and Retrieval: Building Blocks of Modern Databases
For backend engineers and software architects, selecting the appropriate database is rarely a matter of picking the most popular name. At the core of every database lies its storage engine—the internal mechanics that dictate how data is formatted, stored on physical media, and retrieved upon request. In Chapter 3 of Martin Kleppmann’s Designing Data-Intensive Applications, the choice of storage engine is shown to directly impact application performance, write throughput, and query latency.
At its most fundamental level, a storage engine balances two opposing operations:
Key Architectural Trade-Off: Read Acceleration vs. Write Overhead
An index is an auxiliary data structure derived strictly from the primary data. While adding an index accelerates read queries by allowing the database engine to locate specific records without scanning the entire file, it introduces an unavoidable, fundamental tax on write operations. Every time data is inserted, updated, or deleted, every associated index must also be updated. Consequently, storage engine design is governed by an immutable law of system architecture: developers must deliberately select indexing strategies to accelerate critical read patterns without excessively degrading write throughput.
Online Transaction Processing (OLTP) workloads are characterised by high volumes of application requests, where each query typically reads or writes a small number of records by key. To support these access patterns, storage engines rely on specialised data structures.
2.1 Hash Indexes & Append-Only Logs
The simplest key-value storage engine architecture combines an append-only log on disk with an in-memory hash map index (a design pioneered by log-structured key-value stores like Bitcask).
* Write Lifecycle: Every insert or update operation appends a key-value pair directly to a sequential log file on disk. Appending to an existing file involves pure sequential I/O, making write operations exceptionally fast (O(1) complexity). * Read Lifecycle: To locate a value by key, the storage engine looks up the key in an in-memory hash map. The hash map stores the exact byte offset of the data record within the log file on disk. The engine performs an O(1) memory lookup, seeks directly to that byte offset on disk, and reads the record value. * In-Memory RAM Capacity Limitation: The fundamental physical limitation of a hash index is that all keys must fit entirely within volatile RAM. While record values reside on disk, the key space cannot exceed the memory bounds of the machine. * Crash Recovery Mechanisms: Because the hash index exists purely in volatile RAM, restarting the database server destroys the index. The storage engine can recover the index state through two strategies: 1. Complete Log Scanning: Rebuilding the hash map from scratch by scanning the entire append-only log file from start to finish. This process becomes prohibitively slow as log files grow to gigabytes or terabytes. 2. Serialized Index Snapshots: Periodically saving a snapshot of the in-memory hash map to disk. Upon a crash restart, the engine loads the snapshot into memory and only scans the log tail generated after the snapshot timestamp. * Log Compaction and Segment Merging: Because every key update appends a new record to the end of the log, obsolete key versions accumulate continuously. To prevent running out of disk space, the storage engine divides the log into fixed-size segments. A background process continuously executes compaction—scanning segment files, discarding overwritten key entries, keeping only the most recent value for each key, and merging compressed segments into a unified, consolidated file.
2.2 SSTables, Memtables, Write-Ahead Logs (WAL), and LSM-Trees
While simple hash indexes are fast, keeping all keys in an in-memory hash map becomes unfeasible when key counts exceed available RAM. Furthermore, range queries (e.g., scanning keys from user_100 to user_200) are highly inefficient because keys in append-only logs are stored in arbitrary insertion order.
To overcome RAM memory bounds and support range scans, log-structured storage engines transition from unordered logs to Sorted String Tables (SSTables), where key-value pairs are stored on disk strictly sorted by key. Storage engines structured around sorted segments merged concurrently in the background are known as Log-Structured Merge-trees (LSM-trees).
Write Request ──► Write-Ahead Log (Disk WAL) │ ▼ In-Memory Memtable (Skip List / Red-Black Tree) │ ▼ (Size limit reached: Flush as sorted file) SSTable Segment 1 (Disk) ──┐ SSTable Segment 2 (Disk) ──┼─► Background Compaction Merge SSTable Segment 3 (Disk) ──┘
* Sparse In-Memory Index Mechanics: SSTables overcome the RAM bounds of hash maps through sorted key ordering. Because keys are stored sequentially on disk, the database does not need an in-memory index entry for every key. Instead, it maintains a sparse in-memory index containing offset pointers for only a subset of keys (e.g., one key for every few kilobytes of disk block). To look up key K, the engine scans the sparse index to find the nearest key boundaries K_{lower} < K < K_{upper}, seeks directly to K_{lower}'s byte offset on disk, and sequentially scans the small, contiguous disk block. Range queries perform efficient sequential disk scans between range boundaries. * Memtable: An in-memory write buffer implemented as a balanced tree data structure (e.g., a Red-Black tree or Skip List). Incoming inserts, updates, and deletes are inserted directly into the Memtable to maintain all records in strictly sorted key order. * Write-Ahead Log (WAL): Because the Memtable resides in volatile RAM, an unexpected server crash would cause un-flushed writes to be lost. To ensure durability, every write operation is simultaneously appended to an unordered Write-Ahead Log (WAL) on disk before updating the Memtable. The WAL is used exclusively for disaster recovery; once a Memtable is written to disk as an SSTable, its corresponding WAL entries are safely discarded. * Tombstones (Deletion Markers): In append-only log-structured storage, records cannot be removed in-place on disk during a deletion query. Instead, when a key is deleted, the engine writes a special deletion record called a tombstone to the Memtable and WAL. During background compaction, when the engine encounters a tombstone, it purges all historical values of that key alongside the tombstone itself, reclaiming disk space. * Compaction Strategies: Over time, multiple SSTable segment files accumulate on disk. Background compaction threads merge these sorted files using a merge-sort algorithm, purging duplicate key versions and soft-deleted tombstones. LSM-tree engines employ two primary compaction strategies: 1. Size-Tiered Compaction: Used in engines like Apache Cassandra and HBase. Newer and smaller SSTables are merged into progressively larger SSTables once a sufficient number of same-sized segment files accumulate. 2. Leveled Compaction: Used in engines like LevelDB and RocksDB. The dataset is split into discrete "levels" (L_0, L_1, L_2, \dots), and each level is capped at a fixed total size limit. Key ranges are divided across smaller SSTables, guaranteeing non-overlapping key ranges within higher levels (L_1 and above). * Bloom Filters: To resolve point reads for non-existent keys, an LSM-tree engine must search the Memtable and every active SSTable level down to disk, causing high read latency. Storage engines employ Bloom filters—memory-efficient probabilistic data structures—to eliminate unneeded disk seeks. A Bloom filter tests for key set membership in O(1) time; if it returns false, the key is definitively absent from the SSTable segment, allowing the engine to skip the disk block entirely.
Lifecycle of a Write Request in an LSM-Tree Engine
Sequence Component Architecture Phase Primary Function 1 Write-Ahead Log (WAL) Disaster Recovery Logging Sequentially appends write operations to persistent disk storage to guarantee durability across volatile memory crashes. 2 Memtable In-Memory Buffering Inserts the key-value pair into a balanced tree in RAM, maintaining key sorting for incoming reads and future SSTable flushes. 3 SSTable Segment Persistent Disk Flushing Flushes the sorted Memtable sequentially to disk as an immutable SSTable segment file once the in-memory size threshold is reached. 4 SSTable Files Background Compaction Merges overlapping SSTables concurrently on disk via merge-sort, purging obsolete key versions and soft-deleted tombstones.
2.3 B-Trees vs. LSM-Trees: Comparing Storage Philosophies
While LSM-trees structure storage into variable-length sorted segment files, B-Trees remain the standard storage architecture for traditional relational databases. B-Trees divide the database down into fixed-size pages (traditionally 4 KB in size) and update data in-place on disk rather than appending to segment files.
B-Tree Node (4 KB Page on Disk) ┌─────────────────────────────────────────────────────────┐ │ Page Header | Key 100 | Key 200 | Key 300 | Free Space │ └──────┬───────────┬─────────┬─────────┬──────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ [Child 1] [Child 2] [Child 3] [Child 4] <-- Page Pointers
Architectural Dimension B-Trees vs. LSM-Trees Characteristics Write Performance & Ingestion Speed B-Trees: Lower throughput. In-place updates require overwriting full 4 KB pages on disk alongside Write-Ahead Log updates, incurring high random write overhead.<br>LSM-Trees: Higher throughput. Appends writes sequentially to an in-memory Memtable and disk WAL, batching writes into sequential SSTable disk flushes. Read Latency & Lookup Complexity B-Trees: Faster point reads. Direct lookup traverses an O(\log N) page hierarchy to locate exact disk addresses with predictable, low latencies.<br>LSM-Trees: Slower point reads. Lookups must search the Memtable, Bloom filters, and multiple SSTable levels, though Bloom filters reduce unneeded disk seeks. Space Overhead & Fragmentation B-Trees: Higher space fragmentation. In-place page updates leave unused internal space inside pages, and frequent page splits create orphaned space on disk.<br>LSM-Trees: Lower space overhead. Immutable SSTables are written sequentially and compacted continuously, resulting in lower fragmentation and higher compression ratios. Write Amplification & Resource Trade-Offs B-Trees: High write amplification from writing full 4 KB pages for small edits, but disk usage is stable and predictable.<br>LSM-Trees: Lower initial write overhead, but background compaction merges repeatedly rewrite data. Heavy ingestion can trigger compaction stalls by saturating disk bandwidth. Primary Workload Suitability B-Trees: Read-heavy operational workloads requiring rapid point reads, low read latency, and strong transactional stability (OLTP).<br>LSM-Trees: Write-heavy operational workloads requiring high ingestion throughput, time-series data streams, and massive write volume handling.
Database workloads fall into two broad categories: operational transaction processing and analytical processing.
3.1 Operational Databases vs. Data Warehousing
Early enterprise software systems often used a single database for both operational recording and business reporting. However, as business scale increased, combined architectures degraded under resource contention. Modern enterprise architectures cleanly divide operational databases (OLTP) from dedicated analytical data warehouses (OLAP).
[ End-User Applications ] │ (High-frequency, low-latency point reads/writes) ▼ ┌──────────────────────┐ │ OLTP Databases │ (Row-Oriented, Real-Time Operational State) └──────────┬───────────┘ │ │ ETL Pipelines (Extract, Transform, Load) ▼ ┌──────────────────────┐ │ Data Warehouse │ (Column-Oriented Analytics Engine: OLAP) └──────────┬───────────┘ │ (Bulk scans, aggregate queries across millions of rows) ▼ [ Business Intelligence & Analysts ]
3.2 Analytics Schemas: Star Schemas and Snowflake Schemas
Data warehouses employ specialized schema structures designed specifically for analytical aggregations rather than normalized operational structures.
+--------------------+ | Dim_Date | +--------------------+ │ ▼ +--------------------+ +--------------------+ +--------------------+ | Dim_Store |───► | FACT_SALES | ◄───| Dim_Customer | +--------------------+ +--------------------+ +--------------------+ ▲ │ +--------------------+ | Dim_Product | +--------------------+
4.1 Why Column-Oriented Storage?
Traditional OLTP storage engines use row-oriented storage, where all values of a single record are laid out contiguously on disk. This works well for point lookups where an application fetches an entire entity profile.
However, analytical queries rarely require all attributes of a record. An OLAP query typically calculates a single aggregate metric across millions of rows—such as calculating total sales revenue from a table containing 100 columns. In a row-oriented database, the disk engine must read all 100 attributes for every single row off disk into memory before discarding the 99 unneeded fields, creating severe I/O bottlenecks.
Column-oriented storage flips this layout: instead of storing all columns of a single row together, it stores all values of an individual column together in contiguous disk files.
Row-Oriented Layout (OLTP - All fields of Row 1 contiguous): [Row 1: ID, Date, Product, Price] -> [Row 2: ID, Date, Product, Price] -> ...
Column-Oriented Layout (OLAP - All values of a Column contiguous): [File 1 (ID): ID1, ID2, ID3...] [File 2 (Date): Date1, Date2, Date3...] [File 3 (Product): Prod1, Prod2, Prod3...] [File 4 (Price): Val1, Val2, Val3...]
By storing column values contiguously, an analytical query reading 2 columns only reads those 2 specific column files from disk, dramatically reducing I/O bandwidth usage.
4.2 Column Compression, Vectorised Processing & Sort Order
Storing homogenous values sequentially inside column files unlocks powerful compression strategies, as individual columns contain highly repetitive values.
Raw Column Values: [ "US", "US", "US", "US", "IN", "IN", "UK" ] Bitmap (US): 1, 1, 1, 1, 0, 0, 0 ==> Compressed RLE: (4x 1, 3x 0) Bitmap (IN): 0, 0, 0, 0, 1, 1, 0 ==> Compressed RLE: (4x 0, 2x 1, 1x 0)
4.3 Materialised Views and Data Cubes
For analytical queries spanning billions of records, executing on-the-fly aggregations can strain system resources. Analytical engines accelerate these workloads using pre-aggregation techniques.
Selecting the appropriate storage engine requires aligning application access patterns with underlying database mechanics.
Architectural Summary Checklist
Storage Engine Decision Matrix
Use this decision matrix when evaluating storage engines for operational and analytical application architectures:
┌─────────────────────────────────────────┐ │ What is the primary workload requirement?│ └────────────────────┬────────────────────┘ │ ┌──────────────────────────┴──────────────────────────┐ ▼ ▼ ┌──────────────────────────┐ ┌──────────────────────────┐ │ Transactional (OLTP) │ │ Analytical (OLAP) │ └───────────┬──────────────┘ └───────────┬──────────────┘ │ │ ┌──────────┴──────────┐ ┌──────────┴──────────┐ │ Target Access Pattern│ │ Query Pattern │ └──────────┬──────────┘ └───────────┬─────────┘ │ │ ├─► Write-Heavy / Ingestion In-Demand ├─► Scans Subset of Attributes Across Millions of Rows │ └──► Use LSM-Trees (e.g., LevelDB, RocksDB) │ └──► Use Column-Oriented Store (e.g., ClickHouse, Parquet) │ │ └─► Point Reads / Low-Latency Lookups └─► Heavy Aggregations & Instant Summary Reports └──► Use B-Trees (e.g., PostgreSQL, InnoDB) └──► Use Materialised Views / Data Cubes
1. If your application requires low-latency point reads and predictable key lookups: * Recommendation: Select a page-oriented B-Tree engine. 2. If your application demands high write ingestion throughput or processes large streaming logs: * Recommendation: Select an LSM-tree engine utilising SSTables, Memtables, and Bloom filters. 3. If your application requires computing analytical aggregates across millions of rows: * Recommendation: Deploy a Column-Oriented Engine structured using a Star or Snowflake schema, utilising vectorised processing, column compression, and explicit lockstep sorting.