500,000 Rows That Look Real: Load Testing Without Uniform Random Junk
A load test that passes is not automatically good news. Quite often it means the data was too tidy to expose anything.
This is the failure I see most in performance work. Someone generates a million rows, runs the suite, sees acceptable latency, and signs off. Then the feature ships and falls over at a tenth of that volume, and everyone is baffled because the numbers were fine.
The numbers were fine. The distribution was wrong.
Uniform data is the wrong shape
If you generate orders by picking a random user for each one, every user ends up with roughly the same number of orders. With a million orders and ten thousand users, everyone has about a hundred, plus or minus a bit of noise.
Real systems do not look like that at all. A small number of accounts generate most of the activity. A long tail generates almost none. Some accounts signed up and never came back.
That difference is not cosmetic, because most performance problems are specifically problems with the heavy end.
Your customer detail page loads a user's orders. Under uniform data every page load fetches about a hundred rows and everything is fast. Under real data, most pages fetch a handful and one fetches forty thousand. The average looks identical. The experience does not, and the timeout only ever happens to your largest customer, who is also the one you least want to annoy.
What uniformity hides specifically
Missing indexes look unnecessary. With even distribution a sequential scan on a moderate table is tolerable. Add skew and the same query on the heavy account becomes the thing that saturates the database.
N+1 queries stay quiet. A loop that issues one query per related row is fine at ten rows. It is a catastrophe at ten thousand. Uniform data never gives you the ten thousand case.
Pagination bugs hide. Deep offset pagination degrades badly, but only past a page count that uniform data never reaches for any single entity.
The planner makes different choices. Postgres picks plans using statistics about value distribution. Feed it uniform data and it makes reasonable decisions that turn out to be wrong once real skew arrives. You are not just testing with the wrong data, you are testing a different query plan than the one production will use.
Cache hit rates lie. Real access patterns are concentrated, so real caches perform better than uniform ones in some places and much worse in others. Either way your measurement is not transferable.
Volume is the easy half
Getting to half a million rows is not hard. Any loop can do that.
The hard part is making those rows have the right shape:
A few entities holding a disproportionate share of the children. Dates clustered around business hours and weekdays, not spread evenly across the calendar. Text lengths that vary the way human input varies, with a few outliers long enough to matter. Nullable columns that are actually null a realistic proportion of the time, because a code path that never sees a null is a code path you have not tested. Status fields weighted the way real workflows weight them, mostly completed, some pending, a few in the states that only happen when something went wrong.
None of that is exotic. It just does not happen by accident, and random() will not give it to you.
Size the data to the question
One more thing worth saying, because it cuts the work down.
People reach for the largest number they can generate, on the theory that bigger is more rigorous. Usually the useful question is narrower. You are not asking "does the system work at a million rows". You are asking "does this endpoint stay under 200ms when one account has 40,000 orders".
That second question needs a specific shape far more than it needs a big total. Half a million well shaped rows will teach you more than ten million uniform ones, and it will finish generating today.
Decide what you are trying to break before you decide how much data to make.
Where SeedQL fits
SeedQL generates data from your schema with referential integrity intact, at volumes up to 500,000 rows per generation on the Business tier, and pushes it into your target database. Larger runs go through an async job so a long push does not block everything else.
Being precise about the boundary, because performance claims deserve precision. SeedQL gives you volume and structurally valid relationships. Getting a specific skew, such as one account deliberately holding a disproportionate share of rows, is something you shape through how you configure the generation, not something the tool infers from your production statistics on its own. It has never seen your production statistics, which is rather the point.
So treat it as the thing that removes the tedious part, filling a schema correctly at volume, and keep ownership of the interesting part, deciding what shape actually stresses your system.
The check worth running
Before your next load test, run one query against your test dataset. Count the children per parent, order descending, look at the top ten.
If the top row is close to the median row, your data is uniform, and whatever the test tells you afterwards is about a system you do not operate.