Первая SQL-модель
В этом уроке мы напишем свою первую модель, запустим её через dbt run, и разберёмся, что dbt сделал «под капотом» — от SQL-файла до таблицы в DuckDB.
После этого урока вы будете понимать, что такое модель в dbt и какой жизненный цикл она проходит.
Что такое модель
Модель в dbt — это SQL-файл (.sql), содержащий один SELECT-запрос. Этот запрос определяет, что должно быть в результирующей таблице/view.
Простейшая модель:
-- models/customers.sql
SELECT
1 AS customer_id,
'Alice' AS first_name,
'Smith' AS last_name
UNION ALL
SELECT
2 AS customer_id,
'Bob' AS first_name,
'Jones' AS last_name
Этого достаточно. Никакого CREATE TABLE, никаких настроек. Файл лежит в models/customers.sql — для dbt это сигнал «есть модель customers, материализуй её».
При dbt run dbt сделает:
CREATE OR REPLACE VIEW dev.main.customers AS
SELECT
1 AS customer_id,
'Alice' AS first_name,
'Smith' AS last_name
UNION ALL
SELECT
2 AS customer_id,
'Bob' AS first_name,
'Jones' AS last_name
Эти три части — CREATE OR REPLACE VIEW dev.main.<filename> AS <your SELECT> — добавляются dbt автоматически. Имя модели = имя файла (без .sql). Database и schema — из profiles.yml.
Подготовка: создаём проект для упражнения
Если у вас ещё нет проекта от предыдущих уроков:
cd ~/dbt-projects
source .venv/bin/activate
dbt init learning_models
cd learning_models
rm -rf models/example/
mkdir -p models/staging models/marts
Откройте dbt_project.yml, замените секцию models: на:
models:
learning_models:
+materialized: view
marts:
+materialized: table
Это говорит dbt: «по умолчанию все модели — view, кроме marts/* — те таблицы».
Создаём первую модель
Создайте файл models/staging/stg_customers.sql:
-- Простейшая модель: три синтетических клиента
SELECT
1 AS customer_id,
'Alice' AS first_name,
'Smith' AS last_name,
'[email protected]' AS email
UNION ALL
SELECT
2 AS customer_id,
'Bob' AS first_name,
'Jones' AS last_name,
'[email protected]' AS email
UNION ALL
SELECT
3 AS customer_id,
'Carol' AS first_name,
'Davis' AS last_name,
'[email protected]' AS email
Что это:
- Один SELECT с UNION ALL — генерирует 3 строки
- Никаких
CREATE,INSERT,DROP— это decларативное определение содержимого таблицы - Файл в
models/staging/— попадёт под конфигmodels.learning_models.+materialized: view
Запускаем dbt run
dbt run
Вывод:
06:45:11 Running with dbt=1.10.3
06:45:11 Registered adapter: duckdb=1.10.1
06:45:11 Found 1 model, 408 macros
06:45:11
06:45:11 Concurrency: 4 threads (target='dev')
06:45:11
06:45:11 1 of 1 START sql view model main.stg_customers ........................... [RUN]
06:45:11 1 of 1 OK created sql view model main.stg_customers ...................... [OK in 0.04s]
06:45:11
06:45:11 Finished running 1 view model in 0 hours 0 minutes and 0.06 seconds (0.06s).
06:45:11
06:45:11 Completed successfully
06:45:11
06:45:11 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
Разбираемся, что произошло:
- parse — dbt прочитал
models/staging/stg_customers.sql, понял, что это модельstg_customers - render Jinja — в нашей модели Jinja-конструкций нет, поэтому compiled SQL = исходный SELECT
- wrap in DDL — обернул в
CREATE OR REPLACE VIEW dev.main.stg_customers AS (...) - execute — отправил это в DuckDB через
dbt-duckdbadapter
В DuckDB появилась view main.stg_customers.
Проверяем результат через DuckDB CLI
Если у вас установлен duckdb CLI (brew install duckdb на macOS):
duckdb ./dev.duckdb
-- Список таблиц/view в схеме main
.tables
-- stg_customers
-- Показать содержимое
SELECT * FROM stg_customers;
-- ┌─────────────┬────────────┬───────────┬──────────────────────┐
-- │ customer_id │ first_name │ last_name │ email │
-- │ int32 │ varchar │ varchar │ varchar │
-- ├─────────────┼────────────┼───────────┼──────────────────────┤
-- │ 1 │ Alice │ Smith │ [email protected] │
-- │ 2 │ Bob │ Jones │ [email protected] │
-- │ 3 │ Carol │ Davis │ [email protected] │
-- └─────────────┴────────────┴───────────┴──────────────────────┘
-- Проверим, что это view, а не таблица
SELECT table_schema, table_name, table_type
FROM information_schema.tables
WHERE table_name = 'stg_customers';
-- ┌──────────────┬───────────────┬────────────┐
-- │ table_schema │ table_name │ table_type │
-- │ varchar │ varchar │ varchar │
-- ├──────────────┼───────────────┼────────────┤
-- │ main │ stg_customers │ VIEW │
-- └──────────────┴───────────────┴────────────┘
Видим: table_type = VIEW. dbt создал view, не таблицу, потому что project-level default = view.
Альтернатива через Python
Если duckdb CLI не установлен:
import duckdb
con = duckdb.connect('./dev.duckdb')
print(con.execute("SELECT * FROM stg_customers").fetchall())
# [(1, 'Alice', 'Smith', '[email protected]'),
# (2, 'Bob', 'Jones', '[email protected]'),
# (3, 'Carol', 'Davis', '[email protected]')]
Или прямо в Python REPL после активированного venv (duckdb уже установлен как dependency dbt-duckdb).
Меняем materialization на table
Что произойдёт, если переместить модель в models/marts/? Создадим models/marts/customers.sql (тот же контент, что был в staging):
SELECT
1 AS customer_id,
'Alice' AS first_name,
'Smith' AS last_name,
'[email protected]' AS email
UNION ALL
SELECT
2 AS customer_id,
'Bob' AS first_name,
'Jones' AS last_name,
'[email protected]' AS email
UNION ALL
SELECT
3 AS customer_id,
'Carol' AS first_name,
'Davis' AS last_name,
'[email protected]' AS email
Запускаем dbt run:
06:50:11 Found 2 models, 408 macros
06:50:11
06:50:11 Concurrency: 4 threads (target='dev')
06:50:11
06:50:11 1 of 2 START sql view model main.stg_customers ............ [RUN]
06:50:11 1 of 2 OK created sql view model main.stg_customers ....... [OK in 0.03s]
06:50:11 2 of 2 START sql table model main.customers ............... [RUN]
06:50:11 2 of 2 OK created sql table model main.customers .......... [OK in 0.05s]
Заметьте разницу: stg_customers — view model, а customers — table model. Это потому, что models/marts/+materialized: table в dbt_project.yml.
Проверяем в DuckDB:
SELECT table_schema, table_name, table_type
FROM information_schema.tables
WHERE table_name IN ('stg_customers', 'customers');
-- ┌──────────────┬───────────────┬────────────┐
-- │ table_schema │ table_name │ table_type │
-- │ varchar │ varchar │ varchar │
-- ├──────────────┼───────────────┼────────────┤
-- │ main │ stg_customers │ VIEW │
-- │ main │ customers │ BASE TABLE │
-- └──────────────┴───────────────┴────────────┘
customers теперь BASE TABLE (физическая таблица), stg_customers — VIEW.
Что в target/ после dbt run
Посмотрим, что dbt записал в target/:
ls target/compiled/learning_models/models/
# marts/ staging/
cat target/compiled/learning_models/models/staging/stg_customers.sql
Получим:
-- Простейшая модель: три синтетических клиента
SELECT
1 AS customer_id,
'Alice' AS first_name,
'Smith' AS last_name,
'[email protected]' AS email
UNION ALL
SELECT
2 AS customer_id,
'Bob' AS first_name,
'Jones' AS last_name,
'[email protected]' AS email
UNION ALL
SELECT
3 AS customer_id,
'Carol' AS first_name,
'Davis' AS last_name,
'[email protected]' AS email
Это compiled SQL. В нашем простом случае он идентичен исходному (нет Jinja). Когда появится {{ ref(...) }} — здесь будет полное имя таблицы.
А теперь target/run/:
cat target/run/learning_models/models/staging/stg_customers.sql
create or replace view "dev"."main"."stg_customers"
as (
-- Простейшая модель: три синтетических клиента
SELECT
1 AS customer_id,
...
);
Это run SQL — compiled, обёрнутый в DDL. Это то, что dbt реально отправил в DuckDB.
Разница между compiled/ и run/:
- compiled — SELECT в чистом виде, без DDL wrapper. Можете скопировать в SQL editor и выполнить — увидите результат.
- run — то же, обёрнутое в CREATE VIEW / CREATE TABLE / MERGE. Это то, что warehouse увидел.
Эта разница критична для debug-сессий, особенно для incremental моделей, где run-SQL содержит сложную merge-логику.
Несколько способов задать materialization
У нас сейчас materialization идёт через dbt_project.yml. Альтернативы:
1. В config-блоке внутри модели:
{{ config(materialized='table') }}
SELECT * FROM ...
Этот config-блок имеет наивысший приоритет — перебивает project-level и folder-level конфиги. Полезно, когда одна модель в подпапке нуждается в другой materialization.
2. В YAML файле:
# models/staging/_models.yml
version: 2
models:
- name: stg_customers
config:
materialized: incremental
Это менее распространённый способ, но удобен для централизованного управления конфигами множества моделей.
3. По умолчанию (без указания):
Если нигде не задано — materialization = view. Это «дефолт по умолчанию».
Иерархия приоритетов (от низкого к высокому):
- Default (view)
dbt_project.ymlproject-leveldbt_project.ymlfolder-level- YAML model-level
{{ config() }}в .sql
Более специфичный конфиг побеждает.
dbt run vs dbt run —select
dbt run без флагов запускает все модели. На реальном проекте с 500 моделями это занимает 20 минут — обычно вы хотите запускать только то, что меняли.
Использование --select (или короткое -s):
# Только конкретная модель
dbt run --select stg_customers
# Все модели в подпапке staging
dbt run --select staging
# Все модели по тегу (если задан tag)
dbt run --select tag:hourly
# Модель и все upstream (от чего она зависит)
dbt run --select +stg_customers
# Модель и все downstream (что зависит от неё)
dbt run --select stg_customers+
# Модель + 2 уровня downstream
dbt run --select stg_customers+2
Это node selection syntax, подробно в модуле 17. Сейчас знайте про базовый --select <model> — он экономит часы дев-цикла.
Что произошло «под капотом» в DuckDB
Чтобы окончательно щёлкнуло: dbt-duckdb adapter использует Python-binding DuckDB. То есть dbt:
- Открыл
./dev.duckdbчерезduckdb.connect() - Послал готовый SQL через cursor.execute()
- Получил результат, проверил статус
Конкретный SQL, который ушёл в DuckDB:
create or replace view "dev"."main"."stg_customers"
as (
-- Простейшая модель: три синтетических клиента
SELECT
1 AS customer_id,
'Alice' AS first_name,
...
);
DuckDB разбирает это, валидирует, создаёт view в catalog (в файле dev.duckdb), коммитит транзакцию. View весит ~1 KB — это всего метаданные.
Когда вы делаете SELECT * FROM stg_customers, DuckDB разворачивает view в исходный SELECT и выполняет его (3 строки UNION ALL -> 3 строки результата).
Если бы материализация была table:
create or replace table "dev"."main"."stg_customers"
as (
SELECT 1 AS customer_id, ...
UNION ALL ...
);
DuckDB реально материализовал бы данные на диск, и SELECT * FROM stg_customers читал бы готовую таблицу.
Попробуй сам
-
Создайте модель
models/staging/stg_orders.sql:SELECT 100 AS order_id, 1 AS customer_id, '2026-01-01'::date AS order_date, 50.00 AS amount UNION ALL SELECT 101, 2, '2026-01-15'::date, 75.50 UNION ALL SELECT 102, 1, '2026-02-01'::date, 30.00 -
Запустите
dbt run --select stg_orders. Проверьте, что появилась view в DuckDB. -
Создайте
models/staging/_staging__models.yml:version: 2 models: - name: stg_orders config: materialized: table description: "Заказы клиентов" columns: - name: order_id description: "Уникальный ID заказа" -
Запустите
dbt run --select stg_ordersснова. Что изменилось? (Ответ: materialization теперь table — приоритет YAML надdbt_project.yml.) -
Откройте
target/compiled/learning_models/models/staging/stg_orders.sql— увидите тот же SQL, что вы написали. -
Откройте
target/run/learning_models/models/staging/stg_orders.sql— увидите обёрткуcreate or replace table ....
CTE: основа структуры dbt-моделей