Skip to content

Generating sample data

Once a schema is in place you usually want rows to query against — enough of them to make where, group by, order by, and limit interesting. Typing those by hand is tedious. seed fills a table with plausible, generated data in one command, so you can get to querying straight away. It works in both simple and advanced mode, with the same syntax.

One line fills a table with realistic, ready-to-query rows — seed reads each column's name to decide what to make.

The examples use the example library.

seed <table> <count> generates rows and inserts them. If you leave out the count, seed makes 20 rows:

seed members 6
6 row(s) seeded into members
┌───────────┬─────────────────┬────────────┐
│ member_id │ name │ joined │
├───────────┼─────────────────┼────────────┤
│ 1 │ Bret Leffler │ 2023-10-16 │
│ 2 │ Santa Nicolas │ 2024-03-14 │
│ 3 │ Vivienne Barton │ 2024-03-04 │
│ 4 │ Fatima Rippin │ 2022-11-14 │
│ 5 │ Lola Cole │ 2025-05-29 │
│ 6 │ Reina Waters │ 2023-11-25 │
└───────────┴─────────────────┴────────────┘

The data is random, so your rows will differ — run it again for a fresh set, or see Reproducible runs to pin it. member_id was filled automatically (it is a serial column), exactly as it is for an insert; seed leaves serial and shortid columns to the database.

Notice the values are not random noise: name produced believable people and joined produced recent dates. That is because seed reads each column’s name to decide what to generate.

A column’s name chooses a generator, but only when the column’s type fits — a column called email typed int will not get an address. Matching is case-insensitive and looks at the name’s parts (first_name, signup_date, is_active). A representative set:

Column name looks like…You getFor types
name, first_name, last_name, full_namea person’s nametext
emailan email addresstext
username, login, handlea usernametext
phone, mobile, tela phone numbertext
city, country, state, street, zipaddress partstext
company, employer · job, positiona company / job titletext
description, bio, notes, commenta sentence or paragraphtext
url, website · colora URL / hex colourtext
price, amount, cost, salarya money amountint, real, decimal
age · quantity, qty, stocka plausible age / small numberint
date, dob, created_at, updated_ata recent (or birth-window) datedate, datetime
year, *_year, published, founded, birth_yeara plausible year (a birth window for birth_year)int
priority, severity, rating, starsa value from a built-in set (low/medium/high, 1–5, …)text, int
is_active, has_*, enabledtrue / falsebool

When a column’s name isn’t recognised, seed falls back to its type: placeholder words for text, a number for int and real, a recent value for date. A column like isbn or title — text with no specific meaning seed can infer — gets placeholder words; pin it with the set clause if you want something specific.

Two name families are handled specially:

  • Identifier-like names that are not a foreign key or the primary key — code, sku, ref, barcode, a *_id that isn’t a relationship — get unique values, so they read like real identifiers and never collide.
  • Open-ended choice namesstatus, role, type, category, and the like — have no single sensible default, so seed fills them with placeholder text and then tells you to choose the real values yourself. (Common choices that do have a conventional set — priority, severity, rating — are filled from it, per the table above.)

Any column with a unique constraint always gets collision-free values, whatever its name — that is a correctness guarantee, not a guess.

Seed respects relationships. A foreign-key column is filled by sampling from the rows that already exist in the parent table, so every generated reference is valid. Seed the parent first:

seed authors 5
seed books 6
6 row(s) seeded into books
┌─────────┬──────────────────┬───────────┬───────────┬─────────────────────────┐
│ book_id │ title │ author_id │ published │ isbn │
├─────────┼──────────────────┼───────────┼───────────┼─────────────────────────┤
│ 1 │ Austen Wuckert │ 4 │ 1976 │ sit nihil │
│ 2 │ Leanne Fisher │ 3 │ 1961 │ in ex │
│ 3 │ Ludwig Bahringer │ 5 │ 1986 │ sapiente provident │
│ 4 │ Jeff Little │ 3 │ 2021 │ fugit sint eum │
│ 5 │ Kameron Moore │ 1 │ 1995 │ incidunt cumque quia │
│ 6 │ Walker Hammes │ 5 │ 1997 │ enim saepe consequuntur │
└─────────┴──────────────────┴───────────┴───────────┴─────────────────────────┘

Every author_id points at a real author (1–5). Duplicates are expected and correct — one author has many books. published got a plausible year on its own (seed recognises year-like columns); title and isbn are placeholder text, since neither name maps to a real-world generator — pin them with set if you want something specific.

If a parent table is empty, seed refuses rather than inventing a reference that would break the relationship:

cannot seed `books`: parent table `authors` (referenced by `author_id`) has
no rows. Seed or insert into `authors` first.

This mirrors the order you would insert data by hand, and quietly teaches foreign-key dependency order. A junction table linking two parents (a many-to-many bridge) is filled with distinct combinations of the parents’ keys; if you ask for more rows than there are combinations, seed makes as many as it can and tells you.

Add --seed <n> to make a run repeatable: the same number produces the same data, so a teacher can hand out one dataset and a demo stays stable.

seed members 6 --seed 42
6 row(s) seeded into members
┌───────────┬─────────────────┬────────────┐
│ member_id │ name │ joined │
├───────────┼─────────────────┼────────────┤
│ 1 │ Bret Leffler │ 2023-10-16 │
│ 2 │ Santa Nicolas │ 2024-03-14 │
│ 3 │ Vivienne Barton │ 2024-03-04 │
│ 4 │ Fatima Rippin │ 2022-11-14 │
│ 5 │ Lola Cole │ 2025-05-29 │
│ 6 │ Reina Waters │ 2023-11-25 │
└───────────┴─────────────────┴────────────┘

Run that again and you get the very same six members. “The same data” is relative to the table’s current contents: because foreign keys and unique values read the rows already present, reproducibility assumes the same starting point.

Seed’s guesses are a starting point. The optional set clause pins how one or more columns are filled. It reuses syntax you already know from where and update, so there is nothing new to learn — four forms:

FormExampleMeaning
Fixed valueset status = 'active'every row gets the same value
Pick from a listset role in ('admin', 'editor', 'viewer')a random choice from the list
Named generatorset contact as emailforce a specific generator
Rangeset price between 10 and 100a value in the range (also dates)

status has no built-in set — its real values are domain-specific — so it is the natural column to pin:

seed tickets 6 set status in ('open', 'pending', 'closed')
6 row(s) seeded into tickets
┌───────────┬──────────────────────────┬─────────┬──────────┐
│ ticket_id │ subject │ status │ priority │
├───────────┼──────────────────────────┼─────────┼──────────┤
│ 7 │ atque libero │ pending │ high │
│ 8 │ culpa maiores et │ open │ low │
│ 9 │ natus rerum animi │ open │ medium │
│ 10 │ sapiente rem │ closed │ low │
│ 11 │ placeat blanditiis quasi │ closed │ high │
│ 12 │ sed exercitationem │ closed │ low │
└───────────┴──────────────────────────┴─────────┴──────────┘

status takes your values; priority filled itself from its built-in set (low/medium/high). Comma-separate several set clauses to pin more than one column at once.

Text values and list items are quoted ('admin'), exactly as elsewhere; only numbers are bare. Dates in a range are quoted too (set joined between '2023-01-01' and '2024-12-31'). A range on a number column takes numeric bounds, a range on a date column takes date bounds — a mismatched bound is a friendly error.

The named generators you can use after as are:

age, bool, city, color, company, country, date, datetime, email, first_name, job, last_name, name, paragraph, password, phone, price, product, sentence, state, street, url, username, zip.

seed <table>.<column> fills one column across the rows that already exist, rather than adding new rows — the natural follow-up to add column, and the way to repair a single column seed guessed wrongly. Combined with set, it sets that column deliberately:

seed tickets.status set status in ('open', 'closed')
12 row(s) seeded into tickets
┌───────────┬──────────────────────────┬────────┬──────────┐
│ ticket_id │ subject │ status │ priority │
├───────────┼──────────────────────────┼────────┼──────────┤
│ 1 │ ad natus │ closed │ low │
│ 2 │ iusto officia │ closed │ high │
│ 3 │ possimus error │ closed │ high │
│ 4 │ reprehenderit et earum │ open │ low │
│ 5 │ cumque autem voluptas │ open │ low │
│ 6 │ maxime sed sit │ closed │ medium │
│ 7 │ atque libero │ open │ high │
│ 8 │ culpa maiores et │ closed │ low │
│ 9 │ natus rerum animi │ closed │ medium │
│ 10 │ sapiente rem │ closed │ low │
│ 11 │ placeat blanditiis quasi │ closed │ high │
│ 12 │ sed exercitationem │ open │ low │
└───────────┴──────────────────────────┴────────┴──────────┘

Only status changed; the other columns are untouched. Column-fill refuses primary-key and autogenerated (serial / shortid) columns — you do not “fill in” an identity column — and on an empty table it is a no-op.

Open-ended choice columns — status, role, type, and the like — get placeholder text, because there is no single sensible value for them. After a seed, the playground points this out:

seed tickets 6
6 row(s) seeded into tickets
┌───────────┬────────────────────────┬────────────────────────────────────┬──────────┐
│ ticket_id │ subject │ status │ priority │
├───────────┼────────────────────────┼────────────────────────────────────┼──────────┤
│ 1 │ ad natus │ temporibus eos rerum │ low │
│ 2 │ iusto officia │ iure aut provident │ high │
│ 3 │ possimus error │ consequatur consequuntur molestiae │ high │
│ 4 │ reprehenderit et earum │ recusandae est quibusdam │ low │
│ 5 │ cumque autem voluptas │ ea praesentium pariatur │ low │
│ 6 │ maxime sed sit │ sapiente et et │ medium │
└───────────┴────────────────────────┴────────────────────────────────────┴──────────┘

status filled with generic text — they look like fixed value sets. Pin them next time with set status in ('…', '…'), or fix these rows with seed tickets.status set status in ('…', '…').

Here priority was filled from its built-in set automatically, so only status is flagged. The two fixes it suggests are the set clause on the next seed, and column-fill to repair the rows you just made. If a check constraint restricts a column to a list of values (check status in ('open', 'closed')), seed reads that list and uses it automatically — no override needed.

  • The most you can seed at once is 10,000 rows; more is a friendly error (a guard against a typo like seed members 1000000). Seed in smaller batches if you genuinely need more.
  • seed members 0 does nothing.
  • A not null column seed cannot produce a value for — the only real case is a not null blob — makes seed refuse the whole command and name the column, rather than fail partway through.

A whole seed is a single step in the history: one undo removes every row it added, not one row at a time.

seed <Table> [<count>] [set <col> = <value> | in (<value>, ...) | as <generator> | between <low> and <high>][, ...] [--seed <n>]
seed <Table>.<column> [set ...] [--seed <n>]

See also Inserting & editing data, Relationships, Columns, and Constraints.