Skip to content
About
Main content

Retention window versus storage

Simulate a modelNeeds Python

Problem

Open never modify files: a rewrite writes new files and drops the old ones from the current , while older snapshots still reference them for and for queries already running. Expiry and delete files that no snapshot inside the references. Storage is therefore the table plus the rewritten fraction for every day of retention, about size × (1 + f·R), and a vacuum with a window shorter than a running query deletes files under that query.

The chapter's lab uses Iceberg or Delta Lake over ten real days. This run uses a model of the format's file bookkeeping instead (simulated days cannot elapse in real time): a table of 100 files of 128 MB, 10% rewritten every simulated day for 10 days, storage after expiry and vacuum for retention windows of 1, 3 and 7 days, compared with size × (1 + f·R). Then a query that started on an older snapshot reads after a vacuum with a 1-day window.

The question. How much storage does each retention window cost, and what happens to a query older than the window?


Predict

A query reading the day-7 snapshot runs after a vacuum with a 1-day window. What happens?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    retention-window-versus-storage.py59 linesDownload
    View the code
    retention-window-versus-storage.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Retention window against storage (a model of a table format).
    
    A model of an open table format's file bookkeeping, not Iceberg or Delta: a
    table of 100 data files; every simulated day 10% of them are rewritten (new
    files written, old ones dropped from the current snapshot). Expiry then
    vacuum keep every file referenced by a snapshot inside the retention window.
    Storage after 10 days for windows of 1, 3 and 7 days, against
    size × (1 + f·R). Then a query that started on an old snapshot reads after a
    vacuum with a shorter window.
    """
    import argparse, json, random
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--files", type=int, default=100, help="Data files in the table.")
    p.add_argument("--file-mb", type=float, default=128, help="MB per file.")
    p.add_argument("--f", type=float, default=0.1, help="Share of the table rewritten per day.")
    p.add_argument("--days", type=int, default=10, help="Simulated days.")
    p.add_argument("--windows", default="1,3,7", help="Retention windows (days).")
    p.add_argument("--seed", type=int, default=0, help="Random seed.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    def simulate(R):
        rng = random.Random(args.seed); nxt = args.files
        current = list(range(args.files)); snapshots = {0: set(current)}; stored = set(current)
        for day in range(1, args.days + 1):
            out = rng.sample(range(len(current)), int(args.f * args.files))
            for i in out: current[i] = nxt; stored.add(nxt); nxt += 1          # rewrite: new files in, old ones out of the snapshot
            snapshots[day] = set(current)
            snapshots = {d: s for d, s in snapshots.items() if d >= day - R}     # expire snapshots older than the window
            stored = set().union(*snapshots.values())                          # vacuum: delete files no kept snapshot references
        return len(stored) * args.file_mb, snapshots
    base = args.files * args.file_mb; rows = []
    for R in map(int, args.windows.split(",")):
        mb, _ = simulate(R)
        rows.append({"window_days": R, "storage_gb": mb / 1024, "formula_gb": base * (1 + args.f * R) / 1024, "table_gb": base / 1024})
    _, snaps = simulate(1)
    query_snapshot = args.days - 3                                              # a long query started on the snapshot of day 7
    needed = None
    rng = random.Random(args.seed); current = list(range(args.files)); hist = {0: set(current)}; nxt = args.files
    for day in range(1, args.days + 1):
        for i in rng.sample(range(len(current)), int(args.f * args.files)): current[i] = nxt; nxt += 1
        hist[day] = set(current)
    needed = hist[query_snapshot]; kept = set().union(*snaps.values())
    missing = len(needed - kept)
    summary = {"query_snapshot_day": query_snapshot, "files_needed": len(needed), "files_missing": missing,
               "outcome": f"file not found ({missing} of {len(needed)} files deleted)" if missing else "ok"}
    for r in rows: print(f"retention {r['window_days']} day(s): storage {r['storage_gb']:.2f} GB (formula {r['formula_gb']:.2f} GB, table {r['table_gb']:.2f} GB)")
    print(f"query on the day-{query_snapshot} snapshot after a 1-day vacuum: {summary['outcome']}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"summary": summary, "tables": {"windows": rows}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run retention-window-versus-storage.py
    uv run retention-window-versus-storage.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--files100 Data files in the table.
--file-mb128 MB per file.
--f0.1 Share of the table rewritten per day.
--days10 Simulated days.
--windows1, 3, 7 Retention windows (days).
--seed0 Random seed.

Our run

Storage after 10 days

Storage after 10 days
Retention (days)Storage (GB)size × (1 + f·R)Table (GB)
113.813.812.5
316.316.312.5
721.321.312.5
Reference runApple M2 · 8 cores · 8 GB
Output · uv run retention-window-versus-storage.py
retention 1 day(s): storage 13.75 GB (formula 13.75 GB, table 12.50 GB)
retention 3 day(s): storage 16.25 GB (formula 16.25 GB, table 12.50 GB)
retention 7 day(s): storage 21.25 GB (formula 21.25 GB, table 12.50 GB)
query on the day-7 snapshot after a 1-day vacuum: file not found (19 of 100 files deleted)
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

Storage matched the formula exactly: 13.8 GB for a 1-day window, 16.3 GB for 3 days and 21.3 GB for 7, for a table of 12.5 GB. A query reading the snapshot of day 7 after a 1-day vacuum failed: file not found (19 of 100 files deleted).


Read it

The retention window is a storage cost and a promise to readers at the same time: it must be longer than the longest query and every time-travel need, and each extra day costs the daily rewrite volume. In a model, as here, the numbers are exact; on a real table, measure the bucket, because and small appends change the rewrite fraction.