dbt Seeds: Loading Static Reference Data Into Your Warehouse
Not every table your models need comes from a source system. Sometimes itโs a small, hand-maintained lookup โ country code to region mappings, a list of holiday dates, a manually curated list of excluded test accounts โ that doesnโt belong in any production pipeline because nothing external produces it. dbt seeds solve exactly this case: version-controlled CSV files, checked into your repository, loaded into the warehouse as real tables with a single command.
What a Seed Actually Is
A seed is a CSV file living in the seeds/ directory of your dbt project.
country_code,country_name,regionUS,United States,North AmericaCA,Canada,North AmericaGB,United Kingdom,EuropeDE,Germany,EuropeJP,Japan,Asia PacificAU,Australia,Asia Pacificdbt seedRunning this loads the CSV into your warehouse as a real table โ country_region_mapping โ that you can then reference with ref() exactly like any dbt model.
select o.order_id, o.amount, c.regionfrom {{ ref('stg_orders') }} as oleft join {{ ref('country_region_mapping') }} as c on o.country_code = c.country_codeThe fact that country_region_mapping came from a CSV rather than SQL logic is invisible to any model referencing it โ ref() works identically either way, which is the same materialization independence covered in ref() Function.
Why Version-Controlled CSVs Instead of a Manual Warehouse Table
The obvious alternative โ someone manually creates and maintains a lookup table directly in the warehouse โ has a real problem: thereโs no history, no review process, and no way to know who changed a mapping or why. A seed file, being a CSV in Git, gets the same review and audit trail as any code change.
git log seeds/country_region_mapping.csvgit diff HEAD~1 seeds/country_region_mapping.csvAdding a new country to the mapping is a pull request, reviewable by a teammate, with a clear record of exactly what changed and when โ meaningfully different from an untracked manual edit directly in a warehouse console.
Configuring Seed Column Types
By default, dbt infers column types from the CSVโs contents, which usually works but occasionally gets it wrong โ a column of numeric-looking codes that should stay as strings (postal codes, leading-zero IDs) is a common trap.
seeds: my_project: country_region_mapping: +column_types: country_code: varchar(2)Explicitly declaring types for columns where inference is likely to guess wrong avoids a class of subtle bugs where a code like "007" silently becomes the integer 7.
Testing Seeds Like Any Other Model
Seeds support the same schema tests as models and sources โ a seed with a country_code column benefits from the same unique and not_null tests youโd apply anywhere else.
seeds: - name: country_region_mapping columns: - name: country_code tests: - unique - not_nullThis is worth doing precisely because seed files are hand-edited โ a typo introducing a duplicate row is easy to make and easy to miss on review without an automated test catching it.
Referencing Seeds From Downstream Documentation and Lineage
Because seeds are just another node type in dbtโs dependency graph, they appear in the lineage graph exactly like models and sources โ clicking a seed in the documentation site (covered in dbt Docs Serve) shows every downstream model that consumes it. This is genuinely useful for reference data specifically, since it answers a question thatโs otherwise easy to lose track of: โif I change this mapping, what actually depends on it?โ A seed with descriptions and column-level documentation attached is just as discoverable to a new team member as any regular model.
When Seeds Are the Wrong Tool
Seeds are explicitly for small, static, infrequently-changing reference data. Theyโre the wrong tool for:
- Large datasets. Seeds are meant for files in the tens or low hundreds of rows, not tens of thousands โ loading large CSVs through
dbt seedis slow and not what the feature is designed for. - Frequently changing data. If a โreference tableโ actually updates daily, itโs not reference data โ it belongs in a proper source, loaded by your regular ELT pipeline, not maintained as a manually-edited CSV.
- Data that should come from an API or system of record. A currency exchange rate table that should be pulled from a live API shouldnโt be hardcoded as a seed that quietly goes stale.
| Situation | Right approach |
|---|---|
| Small, static lookup, rarely changes | Seed |
| Data owned by another teamโs system | Source |
| Frequently updated business data | Source, not seed |
| Time-tracking snapshots of changing dimensions | Snapshot, covered in dbt Snapshots |
Full Refresh Behavior
By default, dbt seed only inserts new rows into an existing seed table if the schema hasnโt changed โ to fully replace the contents (including deleting removed rows), use --full-refresh:
dbt seed --full-refreshThis drops and recreates the seed table from the current CSV contents, which is usually what you actually want after editing a seed file, since seeds represent the current state of the reference data, not an append-only log.
Common Mistakes
Using seeds for anything resembling operational data. If a seed file needs updating more than occasionally, or by more than one or two people, itโs a sign the data should live in a proper source system instead.
Not specifying column types for ambiguous columns. Numeric-looking string codes are the most common type-inference trap โ always check what type a seed column landed as after the first load.
Forgetting --full-refresh after editing a seed. Without it, deleted rows from the CSV can remain in the warehouse table, since a plain dbt seed run doesnโt necessarily reconcile deletions.
Summary
| Element | Purpose |
|---|---|
seeds/*.csv | Version-controlled static reference data |
dbt seed | Loads CSVs into the warehouse as real tables |
+column_types | Overrides type inference for ambiguous columns |
dbt seed --full-refresh | Fully replaces the seed table, reconciling deletions |
Seeds are a small feature that solves a real and common problem โ small, static reference data that needs the same review discipline as your actual transformation logic, without needing a full source pipeline built around it.