Evidence & benchmarks

Yes — and you don’t have to take our word for it. Every number below comes from a committed script you can run yourself (reproduce it). Latest run: 2026-09-08, fresh PostgreSQL 17 container, raw EXPLAIN plans and stage JSONs in the repo.

The wins, one finding at a time

Missing index: 12× faster

3.57 ms → 0.30 ms (−92%)

The query seq-scanned 50,000 rows to find 100. One CREATE INDEX CONCURRENTLY — the exact remediation pgdba printed — turns it into an index scan.

Why you can trust it: six control queries (lookups, PK hits, small aggregations) ran in every stage and stayed within noise (0.4–0.7 ms). The win is the index, not the machine.

work_mem too small: 31% faster aggregations

391 ms → 269 ms (−31%)

A 1M-row, 100k-group hash aggregation was spilling to disk batches at the 4 MB default. Raising work_mem to 64 MB — pgdba’s proposal, with the temp-file evidence cited — keeps the whole hash in memory (Batches: 1 in the EXPLAIN).

Bloat: 39% of the cost recovered

454 ms → 279 ms (−39%)

With autovacuum disabled and the table churned to 43% dead tuples, the same aggregation paid for 1.75M tuples instead of 1M. VACUUM (ANALYZE) — the remediation pgdba prints — removed the dead tuples and the cost came back down.

Wasteful column types: half the disk, a third faster

24.7 MB → 11.3 MB table (−54%), aggregation 12.1 → 8.4 ms (−31%)

CHAR(1) used as a boolean, NUMERIC(12,0) used as an integer, UUIDs stored as TEXT, JSON used where JSONB fits. The rewrite is the table pgdba’s column checks recommend — same rows, same indexes.

Warning

The benchmark dataset fits in page cache: these are plan-shape and CPU wins, not I/O wins. On a larger-than-RAM workload the cache-sizing story dominates instead. Full caveats in RESULTS.md.

Migration: measured too

330 MB / 1M rows, default engine, default flags: 6.9 s end-to-end (~48 MB/s) with preflight, copy, and exact row-count verification. Details and what that number does and does not mean: Migration speed.

What every check actually says

The check examples gallery shows real captured output from all {{ (index hugo.Data “check-examples”).checks_total }} checks against a deliberately messy database — no hand-written examples.

Receipts that don’t need a benchmark

Reproduce it

# Check-advice wins (container, seeding, 5 stages, column rewrite):
uv run python devdb/bench_check_stages.py

# Migration speed (testbench, 1M rows, default engine):
uv run python devdb/bench_migration.py

Both scripts print their numbers, write JSON, and clean up after themselves. The per-stage JSONs (with EXPLAIN plans) land in devdb/results/check-fresh/.