skip to content

In dbt, what is a seed and what data belongs in one?

level: juniorimportance: must knowfreq 62%

answer

  1. data that lives in the repo
  2. a file you can review in a pull request
  3. small, static, no upstream owner
  4. a CSV dbt loads with dbt seed

basics

~20 s

A seed is a CSV file in the project's seeds directory that dbt loads into the warehouse as a table when you run dbt seed. It suits small, static, version-controlled reference data, not raw source data.

solid answer

~40 s

Seeds are CSV files that live in the repo under the `seeds/` directory (the path is configurable as `seed-paths` in `dbt_project.yml`). `dbt seed` loads each one into the warehouse as a table, and `dbt build` includes them alongside models, snapshots and tests. Because dbt creates the table, you reference a seed with `ref('country_codes')`, never `source()`. The right content is small, slow-moving reference data that has no upstream system of record: country-to-region mappings, fiscal calendar boundaries, an exclusion list of internal test accounts, category lookup tables. The wrong content is raw data — a production table export, an event dump, anything large. dbt loads seeds by generating `INSERT` statements, so big files are slow, they bloat git history with every edit, and anything sensitive ends up in plaintext in version control forever.

code

yaml · 9 lines
yaml
# dbt_project.yml
seed-paths: ["seeds"]

seeds:
  my_project:
    +schema: reference
    country_to_region:
      +column_types:
        country_code: varchar(2)

go deeper

for a junior

Be able to say a seed is a CSV in the repo that dbt seed loads into a table, that models read it with ref(), and that it is for small static reference data.

for a middle

Explain the loading mechanics and the configs you would reach for: truncate-and-insert versus full refresh, column_types, schema placement, and why insert-based loading caps the sensible size.

for a senior

Show where you draw the line between seeded reference data and ingested data, and how you keep seeds documented, tested and reviewable so a mapping change is auditable.

for a principal

Own the policy on what may live in the repo at all — sensitivity, ownership, and the migration path when a seed outgrows a pull-request workflow and needs a real system of record.

## What a seed is A seed is the one place in a dbt project where data itself, rather than SQL that produces data, lives in the repository. You drop a CSV under `seeds/`, run `dbt seed`, and dbt creates a warehouse table whose columns come from the CSV header and whose rows come from the file: ``` seeds/ country_to_region.csv internal_test_accounts.csv fiscal_calendar.csv ``` The directory is configurable — `seed-paths: ["seeds"]` in `dbt_project.yml` — and in dbt versions before 1.0 the default was `data/`, which is why older projects and older blog posts look different. ## How it is loaded and referenced `dbt seed` creates the table if it does not exist, and otherwise truncates it and re-inserts the rows. `dbt seed --full-refresh` drops and recreates it, which you need after changing the CSV's columns or their configured types. `dbt build` runs seeds as part of the full project, in DAG order, so a model that depends on a seed sees the freshly loaded rows. Because dbt creates the object, a seed is a dbt-built node and models reach it with `ref('country_to_region')`. Reaching for `source()` here is a common beginner mistake driven by the intuition that a CSV is "input data". The ownership rule settles it: dbt loads it, so dbt owns it, so `ref()`. Seeds are first-class graph nodes in other ways too. You can document them and test them in a properties YAML under a `seeds:` key — a `unique` test on the lookup key, `not_null` on the value column, a description that shows up in the docs site. ## What belongs in a seed The test is three-part: **small**, **static**, and **owned by the analytics team**. - Mapping tables that exist only for reporting: country to sales region, product code to reporting category, account to owning team. - Business constants with no operational home: fiscal period boundaries, tier thresholds, target values used in a metric. - Deny-lists and allow-lists: internal test accounts to exclude, known bot user agents. - Test fixtures used by CI. What unites these is that the spreadsheet someone maintains *is* the system of record, and putting it in git turns "who changed the region mapping in March?" into a `git log` and a reviewed pull request. ## What does not belong dbt's own guidance is blunt about this: seeds are not for loading raw data. Concretely, avoid: - **Large files.** dbt loads seeds by generating `INSERT` statements in batches. A few hundred or few thousand rows is unremarkable; hundreds of thousands makes every `dbt build` and every CI run slower for everyone. - **Exports of production tables.** That data has a system of record and should be ingested by the loading pipeline, so it stays current without a human committing a file. - **Anything sensitive.** A seed is plaintext in version control and stays in the history even after deletion. Personal data, credentials and anything under an access policy do not belong. - **Frequently changing data.** If it changes daily, a git commit per change is friction and the seed will drift out of date. ## Configuration you will actually use Seed configs go under `seeds:` in `dbt_project.yml`, or per seed in a properties file: ```yaml seeds: my_project: +schema: reference country_to_region: +column_types: country_code: varchar(2) ``` `+column_types` pins types dbt would otherwise infer from the CSV contents; `+schema` places seeds in their own schema; `+quote_columns` controls whether column names are quoted, which matters on warehouses that fold case; `+enabled` turns one off. ## Where seeds sit relative to sources The two are opposites even though both feel like inputs. A source is a table dbt reads but never writes; a seed is a table dbt writes but never derives from anything else. Both are roots of the DAG — nothing upstream of either lives in the project — but only one of them is data you can edit in a pull request. ## What an interviewer is checking Mostly that you know seeds exist, that they are `ref()`-ed, and that you have a defensible line about size and sensitivity. The follow-up is usually "why not just load a big CSV this way?", and the answer is about insert-based loading, repo bloat and the absence of a real system of record — not about a hard row limit, because there is none.

  • Why is a seed referenced with ref() rather than source()?
    Because dbt creates the table. ref() resolves nodes dbt builds — models, seeds and snapshots — while source() resolves tables landed by something outside dbt. Using ref() also puts the seed in the DAG, so dbt loads it before any model that reads it, and a rename or schema change follows automatically.
  • What goes wrong when a team seeds a 500 MB export of a production table?
    Loading is generated INSERT statements, so it is slow and it runs on every full build and every CI job. The file bloats git and every edit produces a huge diff. Worse, the data has a real system of record upstream, so the seed is a fork that goes stale the moment someone forgets to re-export. Ingest it instead.
  • Can seeds be tested and documented like models?
    Yes. In a properties YAML under a `seeds:` key you can give a seed a description, describe its columns, and attach generic tests such as `unique` and `not_null`. They run as part of dbt test and dbt build, and the descriptions appear in the generated docs, which matters because reference mappings are exactly the tables people ask questions about.

saying these in an interview costs you the question

  • References a seed with source() because it is input data
  • Uses seeds to load large exports from production systems
  • Puts personal or confidential data in a version-controlled CSV
  • Thinks dbt seed reads the CSV at query time rather than loading a table
  • Believes seeds must be reloaded manually before every model run

context