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.
The examples use the example library.
Filling a table
Section titled “Filling a table”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.
How columns are filled
Section titled “How columns are filled”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 get | For types |
|---|---|---|
name, first_name, last_name, full_name | a person’s name | text |
email | an email address | text |
username, login, handle | a username | text |
phone, mobile, tel | a phone number | text |
city, country, state, street, zip | address parts | text |
company, employer · job, position | a company / job title | text |
description, bio, notes, comment | a sentence or paragraph | text |
url, website · color | a URL / hex colour | text |
price, amount, cost, salary | a money amount | int, real, decimal |
age · quantity, qty, stock | a plausible age / small number | int |
date, dob, created_at, updated_at | a recent (or birth-window) date | date, datetime |
year, *_year, published, founded, birth_year | a plausible year (a birth window for birth_year) | int |
priority, severity, rating, stars | a value from a built-in set (low/medium/high, 1–5, …) | text, int |
is_active, has_*, enabled | true / false | bool |
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*_idthat isn’t a relationship — get unique values, so they read like real identifiers and never collide. - Open-ended choice names —
status,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.
Foreign keys
Section titled “Foreign keys”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 5seed 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`) hasno 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.
Reproducible runs
Section titled “Reproducible runs”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.
Choosing values yourself
Section titled “Choosing values yourself”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:
| Form | Example | Meaning |
|---|---|---|
| Fixed value | set status = 'active' | every row gets the same value |
| Pick from a list | set role in ('admin', 'editor', 'viewer') | a random choice from the list |
| Named generator | set contact as email | force a specific generator |
| Range | set price between 10 and 100 | a 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.
Filling one column
Section titled “Filling one column”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.
Columns seed can’t guess
Section titled “Columns seed can’t guess”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 │└───────────┴────────────────────────┴────────────────────────────────────┴──────────┘
statusfilled with generic text — they look like fixed value sets. Pin them next time withset status in ('…', '…'), or fix these rows withseed 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.
Limits
Section titled “Limits”- 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 0does nothing.- A
not nullcolumn seed cannot produce a value for — the only real case is anot 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.
Syntax
Section titled “Syntax”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.