Non-reproducing / superseded attempts

Non-reproducing / superseded attempts

Both logs below are real captured runs of this benchmark (not fabricated), kept for honesty about what it took to get a clean signal.

01-console.log — missing ANALYZE, wrong plan for hard_delete_orders

First full run. The churn loop ran VACUUM after every cycle but not ANALYZE, so pg_stats for both tables’ deleted_at column never got real selectivity data. Without stats, Postgres falls back to a fixed default selectivity guess for IS NULL, applied identically to both tables regardless of their true composition. That default guess happened to make the planner pick a Seq Scan + Sort for the small hard_delete_orders table (cost ~1611) instead of the obviously-cheaper Index Scan Backward on its created_at index — see the active_rows EXPLAIN in the log: Seq Scan on hard_delete_orders ... Filter: (deleted_at IS NULL) ... rows=50000. That made hard_delete_orders’ “active rows” query slower than soft_delete’s (p99 ratio 0.08x), the exact opposite of the mechanism the post is about — an artifact of stale statistics, not of bloat.

Fix: the churn loop now runs VACUUM ANALYZE, matching what autovacuum actually does in production. See benchmark.py’s module docstring for the same explanation in-repo.

02-console.log — correct plans, but a weak/noisy latency signal

After the ANALYZE fix, and after switching the removal query so dead rows are interspersed throughout the created_at range (see “within-batch removal” in benchmark.py), both tables picked the right plan (Index Scan for both), and rows_removed_by_filter correctly showed the soft_delete_orders query skipping dead rows. But at the default 5,000-insert / 4,000-remove split (80% within-batch churn) and 300 latency iterations, the actual number of dead rows skipped (~200) was small enough that the real Postgres-side cost difference (tens of microseconds) got lost in Python/psycopg2 round-trip noise: p99 ratio bounced between sub-1x and low-1.x x depending on the run.

Fix: raised REMOVE_BATCH to 4,900 (98% within-batch churn — realistic for a high-turnover entity like sessions/notifications/queue jobs, not just “orders”), raised LATENCY_ITERATIONS to 1,000, and disabled the Python GC during timed loops. That pushes rows_removed_by_filter into the low-thousands, which clears the noise floor and reproduces consistently across runs (see the checked-in ../summary.txt).