Postgres synthetic data: realistic rows, generated from your schema
Synthetic data is the middle ground between random fake rows and copied production dumps: fresh data that behaves like production — foreign keys intact, distributions realistic, no PII by construction. This guide explains what makes synthetic data different, when to use it, and how Weavori generates it from your schema.
Postgres synthetic data is freshly generated data that mimics the structure, constraints, and value distributions of your real database without copying any of its rows. It sits between random fake data — which ignores your schema — and production dumps — which carry your PII. This guide explains the difference, when synthetic data is the right call, and how Weavori generates it from your schema.
What makes synthetic data different
Three properties separate synthetic data from the alternatives:
- Generated, not copied — every row is new; no production value ever appears in the output
- Schema-faithful — foreign keys reference real parent rows, types and constraints are honored
- Distribution-true — column values follow the same shapes as production, sampled from
pg_stats
That last property is what most tools miss. It's also the one that determines whether your tests mean anything.
Synthetic vs. mock vs. anonymized vs. copied
| Fresh synthetic data (Weavori) | Mock/fake data (Faker, random()) | Anonymization (masking) | Production dump (pg_dump) | |
|---|---|---|---|---|
| Input | Your schema (+ pg_stats) | Nothing | Real rows | Real rows |
| Output | New rows | Plausible-looking rows | Transformed real rows | Exact real rows |
| PII risk | None by construction | None | Depends on coverage | Full |
| FK integrity | By construction | Manual | N/A — data exists | Native |
| Config | Zero | Per-column | Per-column rules | None |
| Setup | Client-side CLI | Library | Extension / pipeline | Trivial |
Each answers a different question. Mock data answers "what is a plausible value?" Anonymization answers "how do I use my real data safely?" Dumps answer "how do I get an exact copy?" Synthetic data answers "how do I create data that behaves like mine?"
Why generate synthetic data instead
Three situations push teams to synthetic data:
1. PII is in the way. Copying production into dev, staging, or CI means copying customer data — and every copy is a surface for leaks and a GDPR exposure. Synthetic data has no PII by construction: nothing is copied, so nothing needs masking. It's the cleanest answer to "can we use production data for testing?"
2. Distributions decide whether tests pass. A load test against uniform random data exercises different query plans than production. If 70% of your real status values are active, a flat generator gives you roughly a third of each — and your indexes, joins, and plans all behave differently than they will under real load. Distribution-aware synthetic data makes a 1 GB replica behave like a 1 TB database.
3. Dumps go stale. A dump is a photograph of production at one moment. By the time it's been sanitized, transported, and restored, the schema has moved on. Synthetic data is generated from the current schema — when the schema changes, the data changes with it.
When synthetic data is the wrong tool
Honesty cuts both ways. Synthetic data is not the right call when:
- You need literal fidelity to specific production records (reproducing a customer-reported bug against their exact rows)
- You're masking existing data for compliance — that's anonymization, a different job
- You need a deterministic seed for unit tests — a small hand-written fixture is often simpler
For those, use dumps, masking tools, or fixtures respectively. For everything else — dev, staging, CI, load testing, demos — synthetic data is the fit.
How Weavori generates Postgres synthetic data
Weavori reads the schema as the source of truth and does three things with it:
- Resolves foreign keys by construction — the dependency graph is computed (Kahn's algorithm), parents generate before children, and a streaming FK cache makes every row reference a real parent — O(1) memory per row, even for million-row tables
- Samples real distributions — it reads
pg_stats(most-common values and frequencies) and weights generated values accordingly, so 70%activestays 70%active; numeric columns follow their histogram, not a flat range - Enforces cross-column coherence — temporal ordering between timestamps, conditional nullability, and CHECK constraints are honored per row, so customers don't predate their signups and paid orders have
paid_atvalues
The preview shows the plan before a single row is written — estimated rows per table, generator choices, and any fallbacks. Approve it, and Weavori writes via COPY to the target database or stdout as JSON/CSV.
Privacy is by design
Generation runs entirely client-side. Weavori reads the schema and column statistics (read-only) and never uploads database contents — suitable for regulated environments where data cannot leave the network. Nothing is copied or scrambled, so no production PII ever appears in the output.
Postgres synthetic data in practice
- Staging & dev — every developer gets a right-shaped Postgres copy with zero bytes of production PII: see staging & dev databases
- CI — a fresh, production-shaped database on every push, with API keys, exit codes, and offline licensing: see the CI/CD guide
- Load & performance — distribution-true rows that exercise real query plans: see large-scale generation
- ML & analytics — distribution-true samples for training, without the data
Related guides
- Postgres test data — the five ways to get test data, compared
- Postgres seed data generator — seeding an empty database, compared
- How to generate realistic test data for PostgreSQL — the five practices, in depth
- How Weavori works — introspection, FK resolution, and distribution sampling
- Security — how the CLI handles data and credentials