Chapter 09 · Storage Engines: WAL, B-Trees, LSM Trees, Memtables, SSTables, and Compaction

Compaction Strategies, Tombstones, Bloom Filters, Caches, and Space Amplification

Make compaction, tombstones, Bloom-filter false positives, cache effects, and unsafe delete reclamation observable with a deterministic AtlasMart LSM-state simulation.

Intermediate100–120 minutesTombstone + Bloom + cache labPython 3.13+ · standard libraryVendor-neutral mechanismsLast reviewed: August 2026

Learning outcomes

AtlasMart deletes an obsolete catalog item, yet an old value later reappears after a replica returns. At the same time, read latency oscillates with compaction and a tiny Bloom filter reports many “maybe” results. These symptoms are connected by LSM lifecycle rules: deletion is represented as data, compaction decides when versions can be reclaimed, probabilistic filters save negative I/O but allow false positives, and caches can conceal the underlying device cost.

01

Distinguish size-tiered/universal, leveled, and time-window-style compaction at a conceptual level.

02

Explain tombstones as versioned delete markers and why safe reclamation depends on replica/snapshot/retention assumptions.

03

Demonstrate that Bloom filters can return false positives but must not return false negatives for inserted keys under their model.

04

Show how cache hits change observed read cost without changing underlying storage layout.

05

Diagnose a concrete resurrection caused by reclaiming a tombstone before a stale replica is reconciled.

Tooling and version snapshot · checked 29 August 2026

The mandatory lab uses Python 3.13+ standard library only; it has no database server, driver, container, cloud, or paid-feature dependency. Optional implementation references were rechecked against PostgreSQL 18 documentation (PostgreSQL License), SQLite 3.53.4 (public domain), Apache Cassandra 5.0.9 (Apache License 2.0), and RocksDB 11.1.2 (Apache 2.0 or GPLv2 at the user's option). Product defaults are examples, not portable storage-engine guarantees.

1. Compaction policy chooses where background cost appears

Size-tiered / universal-style compaction generally merges similarly sized or time-adjacent runs, often accepting more overlap to reduce some rewriting. Leveled compaction organizes data into levels and constrains overlap beyond the newest level, usually improving read/space behavior while rewriting data across levels. Time-window strategies group time-correlated data so old windows can age out efficiently when updates rarely cross windows. These are families, not interchangeable product settings.

Strategy lens Often favors Watch closely
Tiered / universal write efficiency / fewer merge steps overlap, read amplification, temporary space
Leveled bounded overlap, read/space efficiency write amplification, compaction bandwidth
Time-window time-series/TTL data with low late mutation cross-window updates, tombstone/expiry assumptions

2. A tombstone is a delete that must defeat older values

Immutable files cannot simply erase an old value in place, so a delete is commonly represented by a tombstone: a newer version saying the key is absent. Reads must let that marker shadow older values. Compaction can eventually remove both the tombstone and the values it dominates—but only after the engine can safely conclude no relevant older copy/snapshot/replica may reintroduce the value.

In distributed databases this couples local compaction to repair/replica convergence. If replica A forgets the tombstone while replica B still holds an older live value, B can later stream that value back. Product-specific grace periods are therefore not magical constants; they encode assumptions about repair cadence, outages, backups/snapshots, and topology.

3. Bloom filters answer “definitely absent” or “maybe present”

A Bloom filter is a compact probabilistic membership structure. Inserted keys set several bits. A lookup whose required bits are not all set is definitely absent from that filter's set; if all are set, the key may exist. Collisions create false positives, which cause unnecessary SSTable/index reads. Correctly implemented Bloom filters do not invent a database row; the real file lookup still verifies the key.

False-positive probability depends on bit budget, number of inserted keys, and number of hash functions. More memory can reduce false positives, but filter memory competes with block cache and other metadata.

4. Caches change the path you observe

A hot block/key served from cache can make a poor on-disk layout look fast during a short benchmark. After restart, eviction, tenant contention, or a working-set shift, the same query may hit multiple files and reveal the true amplification. Measure cache hit ratio and cold/warm phases separately; do not compare two engines when one benchmark is warm and the other cold.

5. Wrong approach: purge delete markers on a fixed short timer

Deleting tombstones aggressively to reclaim space can resurrect data if an offline/stale replica later rejoins. The concrete failure is correctness-visible: a catalog item the business deleted becomes readable again. Repair is not “increase compaction”; it is to preserve the delete until the system's convergence/snapshot assumptions make reclamation safe, then verify with reconciliation histories.

6. AtlasMart lab — false positive, cache masking, and tombstone resurrection

python · compaction_filters.py
import hashlib

class TinyBloom:
    def __init__(self, bits=16, hashes=2):
        self.bits = bits
        self.hashes = hashes
        self.vector = [0] * bits

    def positions(self, key):
        digest = hashlib.sha256(key.encode()).digest()
        return [int.from_bytes(digest[i*4:(i+1)*4], "big") % self.bits for i in range(self.hashes)]

    def add(self, key):
        for p in self.positions(key):
            self.vector[p] = 1

    def might_contain(self, key):
        return all(self.vector[p] for p in self.positions(key))

live = {"sku-1", "sku-2", "sku-3", "sku-4"}
bloom = TinyBloom(bits=12, hashes=2)
for key in live:
    bloom.add(key)
false_positive = next(k for k in [f"sku-{n}" for n in range(5, 200)] if bloom.might_contain(k))
print("Bloom bits:", "".join(map(str, bloom.vector)))
print("known live sku-2 ->", bloom.might_contain("sku-2"))
print("false positive candidate:", false_positive, "->", bloom.might_contain(false_positive))
print("false positive causes extra lookup, not a false row")

# Tombstone lifecycle for product:77.
replica_a = {"product:77": {"version": 3, "kind": "tombstone"}}
replica_b = {"product:77": {"version": 2, "kind": "value", "value": "old-title"}}

# Unsafe: compact A's tombstone away before B has been repaired.
replica_a.pop("product:77")
visible = replica_b["product:77"]["value"] if "product:77" in replica_b else None
print("unsafe early purge -> resurrected value:", visible)

# Safe model: keep tombstone until stale replica receives it, then reclaim later.
replica_a = {"product:77": {"version": 3, "kind": "tombstone"}}
replica_b["product:77"] = dict(replica_a["product:77"])
print("after repair both replicas:", replica_a["product:77"]["kind"], replica_b["product:77"]["kind"])

cache = {}
disk_probes = 0
for _ in range(3):
    key = "sku-2"
    if key in cache:
        source = "cache"
    else:
        disk_probes += 1
        cache[key] = 20
        source = "sstable"
    print("read", key, "from", source)
print("disk probes for three hot reads:", disk_probes)
text · expected output
Bloom bits: 001100101110
known live sku-2 -> True
false positive candidate: sku-11 -> True
false positive causes extra lookup, not a false row
unsafe early purge -> resurrected value: old-title
after repair both replicas: tombstone tombstone
read sku-2 from sstable
read sku-2 from cache
read sku-2 from cache
disk probes for three hot reads: 1

Verification

  • The tiny Bloom filter intentionally finds an absent key that returns “maybe”; the subsequent lookup would still confirm absence.
  • Dropping A's tombstone while B retains version 2 makes the old value visible again in the toy merge path.
  • Propagating the tombstone to B before reclamation preserves deletion.
  • The first hot-key read reaches the simulated SSTable; the next two hit cache, illustrating how cache warmth changes measured I/O.

Check your understanding

  1. Can a Bloom-filter false positive return a nonexistent row to the application?
  2. Why are tombstones retained after the user deletes data?
  3. Why can compaction hurt latency?
  4. When is time-window compaction attractive?
  5. Why report cold and warm cache measurements?
Review the answers

1. No. It causes an unnecessary candidate lookup; the SSTable/index still verifies whether the key exists.

2. They must shadow older immutable/replicated versions until those versions can no longer reappear under the system’s repair/snapshot assumptions.

3. It consumes CPU, read/write bandwidth, cache capacity, and temporary space concurrently with foreground requests.

4. For time-correlated/TTL workloads where data in old windows becomes immutable/expired together and late updates are controlled.

5. A warm cache may hide underlying file probes; cold behavior exposes storage layout and working-set fit.

7. Production judgment

Tune compaction from workload lifecycle and measured debt. Monitor pending compaction bytes/tasks, SSTable/file count, tombstone scans, Bloom false-positive rate where exposed, cache hit ratio, disk bandwidth, temporary space, and p99 latency. Never change tombstone reclamation independently of repair/replica outage assumptions.

Production dimension Questions to record before adopting the mechanism
Correctness / invariants Which writes may be lost, reordered, replayed, or observed through stale storage state? Which invariant is protected above the storage engine?
Durability / recovery What acknowledgement point is durable on the required failure domain? How are WAL replay, checkpoints, backups, replicas, and restore tests verified?
Latency / tails What happens to p95/p99 latency during fsync, page split, flush, compaction, cache miss, or device saturation rather than only during warm steady state?
Amplification / capacity Measure logical versus physical reads/writes and temporary disk headroom. Include compaction debt, tombstones, index/filter memory, and replication overhead.
Device / topology Account for SSD endurance, IOPS, bandwidth, fsync semantics, controller caches, filesystem behavior, replication placement, and failure domains.
Security / tenancy Protect WAL, temporary files, caches, backups, and storage metrics; noisy tenants can create compaction or cache-pressure side channels and denial of service.
Operations / rollback Define telemetry, failure injection, migration, version compatibility, format upgrades, rollback limits, and the skill needed to diagnose storage stalls.
Cost Price storage headroom, SSD replacement/endurance, extra replicas, cache memory, background I/O, backup retention, and engineering time—not only raw GB/month.

Bridge to Lesson 5: We now have the components. The final lesson converts them into a disciplined storage-engine choice based on workload and device evidence.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.