Retention window versus storage
Simulate a modelNeeds Python
Problem
Open table formats never modify files: a rewrite writes new files and drops the old ones from the current snapshot, while older snapshots still reference them for time travel and for queries already running. Expiry and vacuum delete files that no snapshot inside the retention window 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
Storage should follow size × (1 + f·R). A long query that runs across a vacuum with a shorter window fails with file-not-found.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide 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)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
| Knob · default | What 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
| Retention (days) | Storage (GB) | size × (1 + f·R) | Table (GB) |
|---|---|---|---|
| 1 | 13.8 | 13.8 | 12.5 |
| 3 | 16.3 | 16.3 | 12.5 |
| 7 | 21.3 | 21.3 | 12.5 |
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)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 compaction and small appends change the rewrite fraction.