Free datasets
Sample data where the tables actually join.
Most sample data is one flat CSV, which is useless the moment you need a real join, a date dimension, or a number that has to add up. These are multi-table datasets with the foreign keys intact and the arithmetic reconciled. Free, no signup, public domain.
Every claim under each dataset is a check that was run against the actual files. Where something is imperfect, it says so.
Want your own version?
Generate a custom star schema: Retail, Finance, HR, or Supply Chain
These datasets are fixed. The generator lets you configure domain, row count, and time range and download fact + dimension tables shaped to your requirements.
Retail star schema
A dimensional model you can actually practise joins on: one fact table, four dimensions, 63,170 rows, and every join resolving.
Every free mock-data generator hands you one flat CSV, and a flat file has no joins. That makes it useless for the exact skill most people downloading practice data are trying to learn. This is a real star schema: fact_sales joined to date, product, customer and store dimensions, with no orphaned rows and no duplicate keys in any dimension. Two things about it are declared rather than sampled, which is what makes it worth practising on: the category share of revenue is exact to the cent, and monthly revenue follows a stated two-year curve. Your GROUP BY has an answer that is known to be right, so you can check your own SQL instead of eyeballing whether it looks plausible.
| Table | Rows | Columns |
|---|---|---|
| fact_sales | 60,000 | sale_id, date_key, product_key, customer_key, store_key, sold_at, quantity, unit_price, discount_pct, revenue, categorythe fact table; four foreign keys, all resolving |
| dim_customer | 2,000 | customer_key, customer_name, email, city, country, segment |
| dim_date | 730 | date_key, full_date, day_of_week, month_name, quarter, is_weekendtwo full years, for month-over-month and quarter grains |
| dim_product | 400 | product_key, product_name, category, subcategory, unit_cost, list_price |
| dim_store | 40 | store_key, store_name, region, store_type |
What was verified
- Zero orphaned rows joining fact_sales to all four dimensions
- Zero duplicate keys in dim_date, dim_product, dim_customer and dim_store
- Electronics is 42.00% of revenue, Home 26.00%, Apparel 20.00%, Grocery 12.00%
- January 2024 revenue is exactly 820,000.00, as declared
- December 2024 is exactly 1,150,000.00 and December 2025 exactly 1,480,000.00
- Checked with DuckDB against these exact files, which shares no code with the generator
Questions worth asking it
- Which category earns most in which region?
- How does revenue grow month over month across two years?
- Do premium products sell better to Corporate or to Consumer?
- Which stores beat their region's average basket size?
Regenerate it yourself with seed 21 for byte-identical output, or change the story and get a dataset nobody else has.
Machine degradation
100 machines monitored from commissioning to failure, with an exact remaining-useful-life label on every one of 23,118 readings.
A fleet run to failure. Each machine accumulates damage toward a failure time that was declared before any row existed, so remaining useful life is exact rather than annotated afterwards. That is the thing the widely used public predictive-maintenance datasets do not have: in AI4I 2020 tool wear is as likely to fall as to rise between consecutive readings of the same machine, and there is no remaining-life label at all. Four failure modes each drive the measurement they should, sensors go missing in a structured way depending on which control pass was run, and the train and test split is by machine so nothing leaks between them.
| Table | Rows | Columns |
|---|---|---|
| readings | 23,118 | unit_id, cycle, rul_cycles, machine_failure, failure_mode, tool_wear_min, vibration_mm_s, torque_nm, process_temperature_k, air_temperature_k, rotational_speed_rpm, control_type, splitthe benchmark; the latent damage state is deliberately not here |
| units | 100 | unit_id, life_cycles, split, failure_modeone row per machine |
| ground_truth | 23,118 | unit_id, cycle, damagethe latent state, kept out of the benchmark so it cannot leak |
What was verified
- rul_cycles equals life minus cycle on all 23,118 rows
- Exactly one failure row per machine, 100 of 100, each at rul_cycles 0
- tool_wear_min never decreases within a machine, because material does not come back
- Process temperature exceeds air temperature on every observed row
- The latent damage state is absent from the benchmark file
- Split is by machine: 80 train, 20 test, no machine in both
- Sensors go missing by control type, not at random: 9,347 vibration, 7,501 torque, 6,270 air
- heat_dissipation failures run 9K hotter than every other mode at failure
- Baseline: random forest 22.8 cycles MAE on held-out machines, against 58.9 for the mean
Questions worth asking it
- How many cycles of life does this machine have left?
- Which failure mode is this machine heading toward?
- How early can a rising vibration signal be trusted?
- How well does a health indicator recover the true damage state?
Regenerate it yourself with seed 2020 for byte-identical output, or change the story and get a dataset nobody else has.
Ecommerce storefront
A year of orders across five joined tables, with a real Q4 peak and totals that reconcile to the cent.
An online retailer's 2025: customers, a 300-SKU catalogue, orders, line items, and reviews. Every order total is exactly the sum of its own line items, so the joins and the arithmetic both hold when you check them. Demand rises into November and December because there are more orders, not because the orders got bigger, which is how real seasonality works.
| Table | Rows | Columns |
|---|---|---|
| customers | 2,000 | customer_id, full_name, email, city, state, country, signup_date, segment112 signed up and never ordered |
| products | 300 | product_id, product_name, category, unit_price, unit_costevery SKU name distinct |
| orders | 11,081 | order_id, customer_id, order_date, status, channel, order_total |
| order_items | 31,000 | order_item_id, order_id, product_id, quantity, unit_price, line_total |
| reviews | 4,811 | review_id, product_id, customer_id, rating, review_text, review_date |
What was verified
- 0 orphaned foreign keys across all 5 relationships
- 0 orders dated before their customer signed up
- order_total equals the sum of its line items, exactly, for every order
- 0 products priced at or below cost (margins run 22% to 64%)
- Ratings are J-shaped (57% five-star, 7% one-star), not uniform
- Top 10% of customers place 29.5% of orders, a realistic Pareto tail
- 59% of prices end in .99, as real catalogues do
- Every city belongs to its country (London and Newcastle appear under two, correctly)
Questions worth asking it
- Which category carries the best margin, and is it the one selling most?
- How much of revenue comes from the top 10% of customers?
- Do low-rated products actually get returned more often?
- What does the Q4 lift look like split by channel?
Regenerate it yourself with seed 20260721 for byte-identical output, or change the story and get a dataset nobody else has.
B2B SaaS subscription analytics
Accounts, seats, MRR, churn and support load, where company size actually drives the plan.
A B2B SaaS business with 1,200 customer accounts. Company size follows a power law, so most customers are small and a few are large, and the plan each account is on follows from its size rather than being sprinkled at random. Seats fit the plan, MRR is exactly seats times the plan's price, and nobody licenses more seats than they have employees. Support tickets resolve faster as priority rises.
| Table | Rows | Columns |
|---|---|---|
| accounts | 1,200 | account_id, company_name, industry, country, employee_count, signup_date, plan |
| users | 21,884 | user_id, account_id, full_name, role, emailwork emails on the company's own domain |
| subscriptions | 1,200 | subscription_id, account_id, seats, status, mrr, started_on, ended_on |
| invoices | 14,500 | invoice_id, account_id, invoice_date, amount, status |
| support_tickets | 5,600 | ticket_id, account_id, opened_at, priority, category, satisfaction_score, resolution_hours, resolved_at |
What was verified
- 0 orphaned foreign keys, exactly one subscription per account
- 0 invoices or tickets dated before the account existed
- mrr equals seats times the plan's seat price, to the cent, for all 1,200
- 0 accounts licensing more seats than they have employees
- Seats rise with plan: 5, 23, 69, 189 median for Starter to Enterprise
- Churned subscriptions all carry an end date; active ones never do
- Median resolution: 4h urgent, 12h high, 34h normal, 77h low
- 8.4% of tickets are still open, and none of those carry a satisfaction score
- All 21,884 user emails are unique
Questions worth asking it
- Does support load predict churn?
- What is net revenue retention by plan tier?
- Which industry has the worst satisfaction scores?
- How does seat utilisation vary between Starter and Enterprise?
Regenerate it yourself with seed 20260722 for byte-identical output, or change the story and get a dataset nobody else has.
Credit risk portfolio
3,000 borrowers and 4,200 commercial loans, where a credit rating actually predicts whether the loan defaults, at the rate S&P's own data says it should.
A credit_score column and a defaulted column with no statistical relationship between them is worse than useless for validating a risk model. Here, PD comes from S&P Global Ratings' own published annual global corporate default rate (averaged across the 2019-2024 studies), LGD comes from the Basel Foundation IRB supervisory value (a number banks are required to use, not one they estimate), and EAD is drawn plus a credit-conversion-factor on undrawn commitment, the real Basel formula. Whether a loan actually defaults is a Bernoulli draw at its borrower's own PD, so AAA, AA, and A show exactly zero defaults, and CCC defaults at close to 28%, both measured directly from these files.
| Table | Rows | Columns |
|---|---|---|
| borrowers | 3,000 | borrower_id, credit_rating, industry, pdS&P letter grades, not a fictional 1-10 score |
| loans | 4,200 | loan_id, borrower_id, seniority, commitment_type, drawn_amount, undrawn_commitment, origination_date, lgd, ccf, ead, pd, defaulted, expected_loss, realized_lossPD, LGD, EAD, and expected loss, all recomputable from the raw columns |
What was verified
- AAA, AA, and A: 0 of 1,067 loans defaulted (S&P published PD: 0.00% for all three)
- BBB: 2 of 1,261 defaulted, 0.16%, against a cited 0.03% PD
- CCC: 88 of 314 defaulted, 28.03%, against a cited 26.92% PD
- senior_unsecured LGD is exactly 40% on all 3,480 such loans, subordinated exactly 75% on all 720
- ead equals drawn_amount + ccf x undrawn_commitment on all 4,200 loans
- expected_loss equals pd x lgd x ead on all 4,200 loans
- realized_loss is 0 on every non-defaulted loan, and equals lgd x ead on every defaulted one
- 0 orphaned foreign keys joining loans to borrowers
Questions worth asking it
- Does realized default rate actually track PD, by rating grade?
- What is the portfolio's total expected loss as a percentage of exposure?
- How does expected loss concentrate across seniority and commitment type?
- Which industries carry the riskiest rating mix?
Regenerate it yourself with seed 11 for byte-identical output, or change the story and get a dataset nobody else has.
Network intrusion / netflow
500 hosts and 20,000 labeled flows, where a port scan, a DDoS flood, and a brute-force attempt each carry the real statistical signature their MITRE ATT&CK technique actually leaves in traffic.
A protocol column and an is_attack flag with no statistical relationship between them doesn't help test an IDS. A port scan is a single bare TCP SYN swept across a wide, largely non-repeating spread of ports. A DDoS is a wide fan-in of distinct sources converging on one or two targets, each flow tiny. DNS exfiltration is forced onto real UDP/port 53 transport but with an oversized payload. Brute force is repeated short flows against the same few attacker/target pairs on SSH or RDP. Each of those signatures is generated here, not just labeled, grounded in the ID2T / UNSW-NB15 construction methodology and four named MITRE ATT&CK technique IDs.
| Table | Rows | Columns |
|---|---|---|
| hosts | 500 | host_id, zone, ip_addressinternal hosts get RFC 1918 addresses, external hosts don't |
| flows | 20,000 | flow_id, src_host_id, dst_host_id, start_time, attack_cat, protocol, src_port, dst_port, packet_count, byte_count, duration_ms, labelattack_cat and label match each flow's actual packet/byte/port signature |
What was verified
- port_scan: 100% single-packet, 40-60 byte frames on 2,348 flows (a bare SYN, no reply)
- port_scan hosts hit 469 distinct ports on average vs 9.8 for an ordinary benign host
- ddos: 289 distinct sources converge on exactly 2 destinations (593 flows)
- dns_exfiltration: 100% UDP/port 53, averaging 179 bytes/packet vs 80 for this file's own benign DNS traffic (410 flows)
- brute_force: 100% TCP on port 22 or 3389, averaging 34 flows per attacker/target pair vs 1.1 for benign traffic (610 flows)
- no flow implies an impossible sub-40-byte-per-packet frame, across all 20,000 flows
- 0 orphaned foreign keys joining flows to hosts
Questions worth asking it
- Can a detection rule tell a port scan's single-packet SYN spread from ordinary traffic?
- Does a DDoS rule catch the fan-in signature (many sources, few targets) rather than just high volume?
- Is a DNS exfiltration query's payload size actually distinguishable from a normal lookup?
- How concentrated are brute-force attempts on the same attacker/target pair vs benign traffic?
Regenerate it yourself with seed 13 for byte-identical output, or change the story and get a dataset nobody else has.
Contact center queueing (Erlang C)
4 queues, 2,400 half-hour intervals over a full business month, where agents_staffed is what Erlang C (1917) says is actually required to clear each queue's own SLA target, not a plausible-looking number.
A call_volume column and an agents_staffed column with no relationship between them can't test a workforce-management pipeline, because the whole point of WFM is that staffing is derived, not chosen independently. Here, agents_staffed is reverse-solved from Erlang C (computed via the numerically stable Erlang B recursion) to be the smallest agent count that clears the queue's declared SLA target, plus a real 20% shrinkage buffer. Wait probability, average speed of answer, and service level are the exact deterministic Erlang C formulas, recomputable from the raw columns. Abandonment follows the same expected-vs-realized split as the credit-risk dataset's expected_loss/realized_loss: a deterministic rate from a competing-exponentials wait/patience model, and a real Binomial draw for what actually happened.
| Table | Rows | Columns |
|---|---|---|
| queues | 4 | queue_id, queue_name, sla_pct, sla_sec, patience_secBilling Support, Technical Support, Sales, Retention -- each with a different real SLA policy |
| intervals | 2,400 | interval_id, queue_id, interval_start, offered_calls, aht_sec, agents_staffed, traffic_intensity_erlangs, occupancy_pct, wait_probability_pct, asa_sec, service_level_pct, expected_abandon_rate_pct, calls_answered, calls_abandoned, realized_abandon_rate_pctagents_staffed is reverse-solved from Erlang C, not assigned |
What was verified
- every queue clears its own declared SLA target (75-90%) on 100% of its 600 intervals
- service_level_pct matches an independent Erlang C recomputation on all 2,400 intervals
- calls_answered + calls_abandoned equals offered_calls exactly, on all 2,400 intervals
- occupancy_pct and service_level_pct correlate at -0.31 (higher load, worse service)
- aggregate abandonment stays under 4.0% for every queue over the full month
- 0 orphaned foreign keys joining intervals to queues
Questions worth asking it
- Does higher occupancy actually correlate with worse service level, the real Erlang relationship?
- How much staffing buffer (shrinkage) does it take to keep abandonment in an industry-plausible range?
- Does a realistic intraday call-volume curve break a naive flat-staffing assumption?
- Which queue's SLA policy is hardest to hit given its volume and patience profile?
Regenerate it yourself with seed 17 for byte-identical output, or change the story and get a dataset nobody else has.
Manufacturing SPC (Cpk / Xbar-R)
5 processes, 450 subgroups, 2,250 measurements, where process capability and out-of-control detection come from a real Xbar-R control chart -- including one process with an engineered tool-wear drift that actually trips Western Electric rules where it happens.
A measurement column of independently random floats can't sit at a declared Cp/Cpk, can't split into the right within/between variance an Xbar-R chart actually estimates from, and can't trigger a Western Electric Rule violation at a believable rate. Here, sigma is estimated from Rbar/d2 (the real Xbar-R estimator, not pooled standard deviation), control limits are grand_mean +/- A2 x Rbar set during a Phase I baseline, and one process is built off-center on purpose so Cp measurably exceeds Cpk. A fifth process carries a genuine engineered tool-wear drift over its last third of subgroups, tripping Rule 1 and Rule 4 on 100% of its final subgroups and 0% before the drift starts -- the actual signature these rules exist to catch.
| Table | Rows | Columns |
|---|---|---|
| processes | 5 | process_id, part_name, characteristic, unit, lsl, usl, target_cpkreal spec limits and target Cpk spanning excellent to failing |
| subgroups | 450 | subgroup_id, process_id, subgroup_seq, timestamp, xbar, r, rule1_beyond_3sigma, rule3_2of3_beyond_2sigma, rule4_8_consecutive_same_side, out_of_controlWestern Electric rule flags, recomputable from the raw measurements |
| measurements | 2,250 | measurement_id, subgroup_id, process_id, sample_index, value5 per subgroup, one production shift each |
| capability_summary | 5 | process_id, n_subgroups_baseline, grand_mean, rbar, sigma_within, ucl_xbar, lcl_xbar, cp, cpk, target_cpkmeasured Cp/Cpk from the baseline Xbar-R chart |
What was verified
- measured Cpk reconciles to the declared target within sampling tolerance, on all 5 processes
- Cp >= Cpk on every process; 'Housing Bolt Torque' shows Cp 1.94 vs Cpk 1.41 (built off-center)
- 'Valve Seal Thickness' measures Cpk 0.83, below the 1.33 capability minimum -- a real failing example
- stable processes trip a Western Electric rule on 3.3% of subgroups or fewer (a low false-alarm rate)
- 'Bearing Race Width' (tool-wear drift): 100% of its final subgroups trip a rule vs 0% before the drift started
- every subgroup's xbar and r reconcile exactly to its own 5 raw measurements
Questions worth asking it
- Does the measured Cpk (from Rbar/d2) actually reconcile with the declared target?
- How much does an off-center mean cost a process in Cpk vs its potential Cp?
- Can a control-chart rule catch a gradual tool-wear drift before it becomes a defect?
- What does a genuinely failing process (below the 1.33 capability minimum) look like in real subgroup data?
Regenerate it yourself with seed 19 for byte-identical output, or change the story and get a dataset nobody else has.
Need one shaped like your data?
These are fixed samples. For a custom star schema (Retail, Finance, HR, Supply Chain) with your row count and time range, use the generator. Or describe anything in the studio canvas.
Fully synthetic. No real person, company, or transaction is represented, and no production data was read to make these. Released under CC0: use them in portfolios, courses, demos, or products without attribution.

