The first decision is where the eval data comes from. You have three options, and they are not equal for model evaluation.
| Source | Realism | PII risk | Reproducibility | Right for |
|---|---|---|---|---|
| Production live query at eval time | Highest | Highest | Poor: data changes under you | Monitoring, not scoring |
| Anonymized staging snapshot | High | Controllable | High: frozen file | The eval corpus |
| Synthetic generation | Low | None | High | Rare paths, smoke tests |
Synthetic data looks reasonable and passes tests, but it misses the messy reality of production: users who signed up years ago, nulls and encoding issues accumulated through migrations, and field correlations a generator cannot reproduce as the Xata team describes it. Their writeup cites a GitHub authentication bug caused by Turkish dotless I case mapping that synthetic test data would typically miss. That is exactly the kind of case your eval corpus should contain.
Staging sits between the two. The twelve-factor principle of dev/prod parity argues for keeping development, staging, and production as similar as possible, including the same type and version of each backing service, because differences cause code that passed tests to fail in production Twelve-Factor App. Applying that logic to eval: take the snapshot from the environment closest to production that you can legally copy, and keep the format identical to what the model sees at runtime.
Two practical rules for the snapshot:
- Freeze it. Export rows to Parquet or JSONL with a recorded
snapshot_id, a row count and a checksum. Never let the eval job re-read staging at run time. - Bound it. Pull one consistent subset, not the whole database, using a filter that keeps referential integrity. Greenmask, for example, generates the joins needed to keep related tables consistent when you dump a subset Greenmask database subset docs. If your models only see a text field and a few metadata columns, you do not need the customer table at all.



