0%
Chapters / Storage and Retrieval
CHAPTER 03

Storage and Retrieval

A database is a promise about which reads and writes are fast, made concrete by one data structure.

source: DDIA, Chapter 3

3.1Chapter Thesis

A database engine is not a black box with a query language bolted on top - it is one specific data structure, chosen deliberately, that determines exactly which operations are fast and which are slow. Understanding a storage engine means understanding which data structure it committed to, and why that commitment forces everything else.

The central tension is between writes and reads: the fastest possible write is an append to the end of a file, and the fastest possible read is a direct lookup by key. No single data structure gives you both for free - every storage engine is a specific answer to how much read cost it is willing to trade for write speed, or vice versa.

3.2First Principles

Append-only is the cheapest possible write

Sequential writes to the end of a file are dramatically faster than writes at random offsets, because they don't require seeking and they play well with both spinning disks and SSD wear leveling. This single fact is the reason log-structured storage engines exist at all.

An index is a tradeoff, not a free lookup

Any index speeds up the queries it was built for and slows down every write, because the index itself must be updated whenever the underlying data changes. Choosing which fields to index is choosing which future queries you're willing to pay for now.

In-memory index over on-disk data

A hash index mapping every key to a byte offset in an append-only file gives O(1) reads and O(1) writes, as long as the entire index fits in memory. The constraint that makes this fragile is exactly that: the moment the key set outgrows RAM, the mechanism stops working, which is the boundary condition every log- structured engine has to design around.

Compaction reclaims space without random-access rewrites

An append-only log grows forever unless old, overwritten values are reclaimed. Compaction rewrites a segment, keeping only the most recent value for each key, and does so by writing a brand-new segment sequentially - never by mutating the old one in place.

3.3Mechanism

A write appends a length-prefixed key/value record to the active log segment and updates an in-memory hash map from key to (segment, offset). A read looks up the offset in the hash map and seeks directly to it - no scan required. When the active segment crosses a size threshold, it's closed and a new one is opened; a background process merges old, closed segments via compaction, discarding superseded values.

This is precisely the mechanism behind Bitcask-style engines. Its fundamental limitation - the index must fit in memory - is what B-trees and LSM-trees each solve differently: B-trees keep the index itself on disk, organized so a lookup is a small number of disk-page reads; LSM-trees keep the recent index in memory but periodically flush sorted, immutable files (SSTables) to disk and merge them, trading read complexity (checking multiple files) for sustained write throughput.

3.4Build It Yourself

A minimal Bitcask-style engine: append-only log plus an in-memory hash index.

bitcask-engine.ts
interface Location { segment: number; offset: number; }

class BitcaskEngine {
  private log: string[] = [""];
  private index = new Map<string, Location>();

  put(key: string, value: string): void {
    const segment = this.log.length - 1;
    const record = JSON.stringify({ key, value }) + "\\n";
    const offset = this.log[segment].length;
    this.log[segment] += record;
    // index always points at the most recent write -
    // older copies of the same key become dead space.
    this.index.set(key, { segment, offset });
  }

  get(key: string): string | undefined {
    const loc = this.index.get(key);
    if (!loc) return undefined;
    const tail = this.log[loc.segment].slice(loc.offset);
    const line = tail.slice(0, tail.indexOf("\\n"));
    return JSON.parse(line).value;
  }

  // compaction: rewrite only live keys into a fresh segment,
  // sequentially - never mutate a segment in place.
  compact(): void {
    let merged = "";
    const newIndex = new Map<string, Location>();
    for (const [key] of this.index) {
      const value = this.get(key)!;
      const offset = merged.length;
      merged += JSON.stringify({ key, value }) + "\\n";
      newIndex.set(key, { segment: 0, offset });
    }
    this.log = [merged];
    this.index = newIndex;
  }
}
Tip
Level 3 (Failure) needs a crash-recovery path: on startup, replay every segment from offset zero, rebuilding the index from scratch, and detect a truncated final record by checking the length prefix before trusting it.

3.5Failure Modes

ConditionResulting state
Process crashes mid-writeThe final record in the active segment may be truncated; without a checksum or length check, replay can either skip it correctly or, worse, misparse it as a different valid record.
Process restartsThe in-memory hash index is gone and must be rebuilt by replaying every segment from the start - the more segments exist, the longer recovery takes.
Compaction runs concurrently with a readA reader holding a stale (segment, offset) pointing at a segment being merged can read garbage or a since-deleted value unless compaction publishes the new segment atomically and only then retires the old one.

3.6Tradeoffs

Engine familyBenefitCostWhen it makes sense
Hash index over append-only logO(1) reads and writes; trivially simple crash recovery via replayFull key set must fit in memory; range queries are not supportedSmall, bounded key spaces with point lookups only
LSM-tree (sorted, merged files)Sustained high write throughput; supports range queriesReads may check multiple files; compaction consumes background I/OWrite-heavy workloads at large key-space scale
B-tree (in-place page updates)Predictable read latency; mature transactional supportRandom-access writes to pages are more expensive than sequential appendsRead-heavy or mixed workloads needing strong transactional guarantees

3.7Production Perspective

Real log-structured engines (LevelDB, RocksDB) generalize this exact mechanism into multiple levels of sorted files, using bloom filters to avoid checking files that can't contain a key and background compaction threads tuned to balance write amplification against read amplification - the same tradeoff this minimal engine makes explicit at a much smaller scale.