Learning Platform
Глоссарий Troubleshooting
Урок 05.01 · 17 мин
Начальный
modeldbt-runmaterializationfirst-steps

Первая 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

Разбираемся, что произошло:

Жизненный цикл модели при dbt run
parsedbt сканирует все .sql и .yml файлы, строит DAG, разрешает ref() и source(). Результат — manifest.json
render JinjaРаскрытие всех Jinja-конструкций: {{ ref(...) }}, {{ config(...) }}, {{ var(...) }}. Результат — pure SQL без Jinja
wrap in DDLОбернуть compiled SQL в CREATE TABLE/CREATE VIEW в зависимости от materialization
executeОтправить финальный SQL в warehouse через adapter (DuckDB Python client для нас)
  1. parse — dbt прочитал models/staging/stg_customers.sql, понял, что это модель stg_customers
  2. render Jinja — в нашей модели Jinja-конструкций нет, поэтому compiled SQL = исходный SELECT
  3. wrap in DDL — обернул в CREATE OR REPLACE VIEW dev.main.stg_customers AS (...)
  4. execute — отправил это в DuckDB через dbt-duckdb adapter

В 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_customersview model, а customerstable 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. Это «дефолт по умолчанию».

Иерархия приоритетов (от низкого к высокому):

  1. Default (view)
  2. dbt_project.yml project-level
  3. dbt_project.yml folder-level
  4. YAML model-level
  5. {{ 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:

  1. Открыл ./dev.duckdb через duckdb.connect()
  2. Послал готовый SQL через cursor.execute()
  3. Получил результат, проверил статус

Конкретный 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 читал бы готовую таблицу.


Попробуй сам

  1. Создайте модель 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
  2. Запустите dbt run --select stg_orders. Проверьте, что появилась view в DuckDB.

  3. Создайте models/staging/_staging__models.yml:

    version: 2
    models:
      - name: stg_orders
        config:
          materialized: table
        description: "Заказы клиентов"
        columns:
          - name: order_id
            description: "Уникальный ID заказа"
  4. Запустите dbt run --select stg_orders снова. Что изменилось? (Ответ: materialization теперь table — приоритет YAML над dbt_project.yml.)

  5. Откройте target/compiled/learning_models/models/staging/stg_orders.sql — увидите тот же SQL, что вы написали.

  6. Откройте target/run/learning_models/models/staging/stg_orders.sql — увидите обёртку create or replace table ....


CTE: основа структуры dbt-моделей
Проверка знанийKnowledge check
Что произойдёт, если положить файл models/my_model.sql с одной строкой "SELECT 1 AS id" и запустить dbt run — пройдут ли все четыре фазы (parse, render, wrap, execute)?
ОтветAnswer
Да, все четыре фазы пройдут успешно. parse: dbt просканирует models/, найдёт my_model.sql, определит модель my_model без зависимостей (нет ref/source). render: Jinja-рендеринг не изменит SQL, потому что Jinja-конструкций нет — compiled SQL = "SELECT 1 AS id". wrap: dbt обернёт в DDL по дефолту "create or replace view dev.main.my_model as (SELECT 1 AS id)" — view, потому что нигде не указана другая materialization. execute: dbt отправит этот SQL через duckdb adapter, DuckDB создаст view в файле dev.duckdb. После этого SELECT * FROM my_model вернёт одну строку (1). Это абсолютный минимум жизнеспособной dbt-модели — никаких import, конфигов, тестов не требуется. Концепт «всё, что нужно — SELECT» — фундаментальная сила dbt.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что является минимальным содержимым жизнеспособной dbt-модели?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4