Seeds: CSV как первоклассный citizen
В dbt существует три способа доставить данные в warehouse:
- Source — данные уже лежат в warehouse, их туда положил кто-то другой (Fivetran, Airbyte, ручной INSERT). dbt просто декларирует, что эти таблицы существуют.
- Seed — CSV-файл лежит в git-репозитории dbt-проекта. Команда
dbt seedчитает CSV и заливает его в warehouse как таблицу. - Snapshot — отдельный механизм для SCD2 (Slowly Changing Dimensions). Об этом — в следующих трёх уроках.
Этот урок целиком про seeds. Звучит просто: «CSV -> таблица». На практике seed — это критический паттерн для маленьких справочников, и есть жёсткое правило 5 MB, которое отделяет «хороший seed» от «git-репозитория, который тормозит на каждом checkout».
Зачем нужны seeds
Представьте, что вы строите маркетинговый mart, и вам нужна таблица соответствия country_code -> country_name -> region. Откуда её взять?
Варианты:
- Хардкод в SQL —
CASE WHEN country='US' THEN 'United States' WHEN ...на 200 строк. Нечитаемо, дублируется в каждой модели, нельзя пере-использовать. - Загрузить через source — поднять отдельный pipeline Fivetran для CSV из Google Sheets. Overkill для 200 строк.
- Положить CSV в репозиторий и сделать seed —
seeds/country_codes.csv+dbt seed. Таблица появляется в warehouse, на неё можно делатьref('country_codes')как на обычную модель.
Seed — это версионированные данные. CSV лежит в git, поэтому:
- Code review показывает diff: «инженер добавил Beirut в country_codes — нормально».
- История изменений — git blame покажет, кто и когда поменял маппинг.
- Развёртывание атомарно: новая модель + новые данные катятся одним PR.
Где живут seeds
В стандартном dbt-проекте директория seeds/ лежит в корне:
jaffle_shop/
dbt_project.yml
models/
seeds/
country_codes.csv
product_categories.csv
snapshots/
tests/
macros/
Путь настраивается в dbt_project.yml:
seed-paths: ["seeds"]
Можно изменить на ["data"] или указать несколько директорий — но 99% проектов оставляют дефолт.
Анатомия CSV-файла seed
Минимальный seed выглядит так. Файл seeds/country_codes.csv:
country_code,country_name,region
US,United States,Americas
GB,United Kingdom,EMEA
JP,Japan,APAC
DE,Germany,EMEA
BR,Brazil,Americas
Правила, которым следует dbt:
- Первая строка — имена колонок. dbt использует их как имена столбцов в таблице.
- Разделитель — запятая (по умолчанию). Можно поменять, но об этом — в следующем уроке.
- Имя файла = имя таблицы.
country_codes.csvсоздаст таблицуcountry_codesв warehouse. - Типы данных автоматические. dbt пытается угадать тип по содержимому: строки идут в TEXT/VARCHAR, числа — в INTEGER/DOUBLE. Об этом тоже в следующем уроке (часто угадывает плохо, и нужно явно задавать).
dbt seed — что делает команда
Запустите:
dbt seed
Что произойдёт под капотом:
- dbt находит все CSV в
seed-paths. - Для каждого файла:
- Парсит CSV (использует
csv.DictReaderPython). - Угадывает типы колонок (или читает из
column_typesconfig). - Дропает существующую таблицу (
DROP TABLE IF EXISTS ...). - Создаёт пустую таблицу нужной схемы.
- INSERT’ит строки батчами (по умолчанию батч =
min(10000, # rows)).
- Парсит CSV (использует
- Возвращает run-result: успешные/упавшие seeds.
Типичный вывод:
$ dbt seed
14:21:03 Running with dbt=1.10.2
14:21:03 Registered adapter: duckdb=1.10.1
14:21:04 Found 2 seeds
14:21:04
14:21:04 Concurrency: 4 threads (target='dev')
14:21:04
14:21:04 1 of 2 START seed file main.country_codes ............... [RUN]
14:21:04 2 of 2 START seed file main.product_categories .......... [RUN]
14:21:04 1 of 2 OK loaded seed file main.country_codes ........... [INSERT 195 in 0.12s]
14:21:04 2 of 2 OK loaded seed file main.product_categories ...... [INSERT 24 in 0.05s]
14:21:04 Finished running 2 seeds in 0:00:01
14:21:04 Completed successfully
После этого таблицы country_codes и product_categories существуют в DuckDB. Проверим:
-- В DuckDB (или через dbt show)
SELECT * FROM country_codes LIMIT 3;
| country_code | country_name | region |
|---|---|---|
| US | United States | Americas |
| GB | United Kingdom | EMEA |
| JP | Japan | APAC |
Использование seed в модели через ref()
После dbt seed таблица существует в warehouse. Чтобы использовать её в модели, делаете ref(), точно так же как на обычную модель:
-- models/marts/customers.sql
SELECT
c.customer_id,
c.country_code,
cc.country_name,
cc.region
FROM {{ ref('stg_jaffle__customers') }} c
LEFT JOIN {{ ref('country_codes') }} cc
ON c.country_code = cc.country_code
Важно: seed — node в DAG, у него есть depends-on. Если запустить dbt run --select customers, dbt не запустит seed автоматически (это не source, это нода). Чтобы прогнать seed перед моделью, используйте dbt build или явный +:
dbt build --select +customers # запустит seed -> staging -> customers + тесты
dbt seed --select country_codes # только этот seed
dbt run --select country_codes+ # seed + все downstream
dbt build — самая удобная команда для CI/CD: она запускает seeds -> snapshots -> models -> tests в правильном порядке по DAG. Если кто-то изменил CSV, новые данные попадут в warehouse одной командой.
Когда брать seed, а когда — нет
Главный признак — размер и динамика данных.
Правила, которые работают:
- менее 5 MB или менее 50 000 строк -> seed это нормально.
- Меняется реже раза в день -> seed нормально. Если чаще — нужен внешний pipeline.
- Хочу видеть изменения в PR -> seed (потому что diff в git).
- Не хочу видеть изменения в PR -> source (потому что это «грязные» внешние данные).
Жёсткое правило 5 MB
Почему именно 5 MB? Это не магическое число dbt, это практика git и code review.
CSV — это plain text. Файл на 50 000 строк (типичная маленькая lookup-таблица) — 2–5 MB. Файл на 500 000 строк — 50 MB. Что происходит с репозиторием при 50 MB seed:
git cloneтормозит. Не на 5 секунд, а на 30–60 на медленной сети. Каждый новый разработчик/CI-раннер.git statusтормозит. Git считает hash каждого файла; крупные текстовые файлы дольше хешируются.- Code review бесполезен. GitHub не показывает diff > 1 MB по умолчанию. Reviewer не увидит, что строка 32 451 поменялась с «Tashkent» на «Toshkent».
- History раздувается. Каждое изменение CSV хранится в
.git/целиком. 10 правок по 50 MB = 500 MB в.git/objects/.
Если данные больше 5 MB — это не lookup-таблица, а датасет. Загружайте через source.
Если уже добавили большой CSV и закоммитили — git rm его не уменьшает репозиторий. Файл остаётся в history. Чистка через git filter-repo или BFG Repo-Cleaner — это операция, которая ломает hashes commit’ов у всей команды. Лучше не доводить.
Что не делает seed
dbt seed — это примитивный механизм. Он не умеет:
- Append. Каждый запуск делает
DROP + CREATE + INSERT, то есть полностью пере-загружает таблицу. Если нужен инкремент — это не seed, это incremental model. - JOIN с другими seed. Seed читает CSV как есть, никакой SQL-обработки.
- Парсить сложные форматы. CSV-only. JSON/Parquet/XLSX не поддерживается из коробки (можно загрузить через
read_csv/read_parquetsource-конфиги в DuckDB, но это уже source). - Транзакционно атомарно. На большинстве warehouse
DROP TABLEкоммитится сразу, поэтому в окне между DROP и INSERT таблица не существует. Downstream запросы упадут с «relation not found». Для production это редко проблема (seed редко запускается), но знать стоит.
Попробуй сам
Создайте файл seeds/country_codes.csv в вашем dbt-проекте:
country_code,country_name,region
US,United States,Americas
GB,United Kingdom,EMEA
JP,Japan,APAC
RU,Russia,EMEA
CN,China,APAC
Запустите:
dbt seed
dbt show --inline "SELECT * FROM {{ ref('country_codes') }}"
Теперь измените одну строку в CSV — например, «Russia» -> «Russian Federation». Запустите dbt seed снова. Заметьте: dbt не делает «UPDATE country_name SET …». Он делает полный DROP + CREATE + INSERT. Если у вас downstream-модели читают эту таблицу — они увидят новые данные после следующего dbt run.
Бонус-задача: сделайте простую модель models/marts/customers_enriched.sql, которая JOIN-ит staging-таблицу клиентов с country_codes через ref('country_codes'). Запустите dbt build --select +customers_enriched — обратите внимание, что dbt запустит seed автоматически перед моделью.
Ключевые выводы
- Seed — это CSV в git-репозитории, который dbt загружает в warehouse как обычную таблицу. Доступен через
ref('seed_name')так же, как модель. - Используется для маленьких lookup-таблиц менее 5 MB: country codes, mappings, configs. Версионируется в git, изменения видны в PR.
- Не для больших датасетов. > 5 MB ломает git workflow: clone тормозит, diff не показывается, history раздувается.
dbt seedделает полный DROP + CREATE + INSERT каждый раз. Нет append, нет incremental.dbt build— правильная команда: запускает seeds -> snapshots -> models -> tests в порядке DAG.- Seed — это нода в DAG.
dbt runсам по себе не запустит seed; нужноdbt seed,dbt build, или явное включение в селектор.