Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
The temptation when generating test data is to make everything uniform — every user gets roughly the same number of orders, every status appears equally often — because it is easy and it looks tidy. It also destroys the entire point of the exercise. A query planner estimates how many rows a filter will return by assuming values are spread evenly within a bucket, and on uniform data that assumption is true, so the estimates are perfect and you never see an estimate go wrong. Real data is not like that: a few customers place thousands of orders while most place one, 85% of orders share a single status, and a handful of values dominate every column. Those lopsided distributions are precisely where estimates collapse and the planner picks a nested loop over a million rows. Generating them on purpose is what makes modules 5 and 8 possible.
The two distributions the generator creates, what each one is for, and the query that proves the skew arrived intact.
-- ══ SKEW 1: ORDERS PER USER, A POWER LAW ═══════════════════════
--
-- The generator draws user_id as:
-- 1 + (N_USERS * power(random(), 3))::bigint
--
-- Cubing a uniform draw concentrates the result near the low end, so
-- low-numbered users get most of the orders. The effect:
SELECT n_orders, count(*) AS n_users FROM (
SELECT user_id, count(*) AS n_orders FROM orders GROUP BY 1
) s GROUP BY 1 ORDER BY 1 LIMIT 6;
-- n_orders | n_users
-- ----------+---------
-- 1 | 88412
-- 2 | 31024
-- 3 | 14882
-- 4 | 8201
-- 5 | 5410
-- 6 | 3902
SELECT user_id, count(*) c FROM orders GROUP BY 1 ORDER BY c DESC LIMIT 3;
-- user_id | c
-- ---------+------
-- 3 | 4102
-- 7 | 3880
-- 11 | 3401
--
-- Most users have one order; a few have thousands. THIS is what makes
-- module 5's nested-loop catastrophe reproducible — the planner's
-- average-orders-per-user estimate is wrong for both ends.
-- ══ SKEW 2: CATEGORICAL VALUES ═════════════════════════════════
SELECT status, count(*),
round(100.0 * count(*) / sum(count(*)) OVER (), 2) AS pct
FROM orders GROUP BY status ORDER BY 2 DESC;
-- status | count | pct
-- ----------+---------+-------
-- paid | 1700411 | 85.02
-- shipped | 239660 | 11.98
-- cart | 200220 | 10.01
-- refunded | 59921 | 2.99
SELECT plan, count(*), round(100.0*count(*)/sum(count(*)) OVER (),2) AS pct
FROM users GROUP BY plan ORDER BY 2 DESC;
-- plan | count | pct
-- ------------+--------+-------
-- free | 176012 | 88.01
-- enterprise | 16021 | 8.01
-- pro | 8102 | 4.05
-- ══ WHY THIS MATTERS, CONCRETELY ═══════════════════════════════
--
-- 85% vs 3% is an enormous selectivity spread on ONE column. It makes
-- three separate lessons possible:
--
-- module 4 a sequential scan is CORRECT for status='paid' (85%)
-- and wrong for status='refunded' (3%)
-- module 8 a prepared statement's generic plan cannot be right for
-- both, which is the sixth-execution cliff
-- module 7 a partial index on the rare value is 3% of the size
--
-- On uniform data — 25% per status — not one of those lessons exists.
-- ── Verify the skew survived the load ──────────────────────────
SELECT
round(stddev(c) / avg(c), 2) AS coefficient_of_variation
FROM (SELECT count(*) c FROM orders GROUP BY user_id) s;
-- coefficient_of_variation
-- --------------------------
-- 4.18
--
-- Above ~2 means genuinely lopsided. Near 0 means your load produced
-- uniform data and the later modules will not demonstrate what they
-- claim — reseed before continuing.