Learning Platform
Глоссарий Troubleshooting
Урок 13.03 · 22 мин
Начальный
dbttargetdev vs prodenvironments

Dev vs Prod: паттерн переключения через target.name

Финальный урок модуля — практический. Мы поставим вместе всё, что изучили: var, env_var, target, {% if %}, и научимся делать модели, которые корректно работают в dev (быстро, на маленьком объёме) и в prod (полная история, корректные схемы, full performance).

Это тот самый паттерн, который вам нужен для запуска dbt-проекта в production. Без него — либо разработка медленная (на полном объёме), либо есть риск что-то сломать в prod при экспериментах.

Зачем разделять dev и prod

Главный механизм: target

target в dbt — это именованная конфигурация подключения к warehouse. Объявляется в profiles.yml. Один профиль может иметь несколько targets (dev, ci, prod), переключение через --target.

# ~/.dbt/profiles.yml
jaffle_shop:
  target: dev  # default target, если --target не передан
  outputs:
    dev:
      type: duckdb
      path: "{{ env_var('DBT_DUCKDB_PATH', 'dev.duckdb') }}"
      schema: "dbt_{{ env_var('USER', 'dev') }}"
      threads: 4
    
    ci:
      type: duckdb
      path: "/tmp/ci_{{ env_var('CI_BUILD_ID', 'local') }}.duckdb"
      schema: dbt_ci
      threads: 8
    
    prod:
      type: postgres
      host: "{{ env_var('PROD_PG_HOST') }}"
      user: "{{ env_var('PROD_PG_USER') }}"
      password: "{{ env_var('PROD_PG_PASS') }}"
      port: "{{ env_var('PROD_PG_PORT', '5432') | int }}"
      dbname: analytics
      schema: dbt_prod
      threads: 8

Запуск:

# Dev (default)
$ dbt run

# CI 
$ dbt run --target ci

# Prod
$ dbt run --target prod

Каждый target — изолированная конфигурация. Switch — одной флагой.

target.X в Jinja

Из любого шаблона можно прочитать атрибуты текущего target:

{{ target.name }}      -- 'dev', 'ci', 'prod'
{{ target.type }}      -- 'duckdb', 'postgres', ...
{{ target.schema }}    -- 'dbt_alice', 'dbt_ci', 'dbt_prod'
{{ target.database }}  -- название БД (для DuckDB — путь к файлу)
{{ target.threads }}   -- 4, 8, ...
{{ target.user }}      -- имя пользователя (Postgres)

target.name — это главное условие переключения. С его помощью модели меняют поведение.

Паттерн 1: фильтр в dev на N дней

Самый частый. В prod — полная история, в dev — последние 7 дней.

-- models/staging/stg_jaffle__orders.sql

select *
from {{ source('jaffle', 'raw_orders') }}
where 1=1

{% if target.name == 'dev' %}
    and order_date >= current_date - interval 7 day
{% endif %}

Compile в dev:

select *
from "jaffle_shop"."main"."raw_orders"
where 1=1
    and order_date >= current_date - interval 7 day

Compile в prod:

select *
from "analytics"."raw"."raw_orders"
where 1=1

Время сборки моделей в dev падает с минут до секунд.

Вариация: переменная lookback

Часто dev на 7 дней мало (нужно хотя бы 30 для теста по месячной агрегации). Сделаем параметрическим:

{% set is_dev = target.name == 'dev' %}
{% set lookback_days = var('lookback_days', 7 if is_dev else none) %}

select *
from {{ source('jaffle', 'raw_orders') }}

{% if lookback_days %}
where order_date >= current_date - interval '{{ lookback_days }}' day
{% endif %}

В dev — WHERE с 7 днями. В prod — нет WHERE. Можно override:

# Dev с 30 днями
$ dbt run --target dev --vars '{lookback_days: 30}'

# Prod backfill с явным WHERE
$ dbt run --target prod --vars '{lookback_days: 365}'

Паттерн 2: отдельные схемы для каждого dev-разработчика

В команде из 5 разработчиков никто не хочет работать в общей dbt_dev схеме — конфликтуют таблицы, ломают друг другу runs.

Решение: схема включает имя пользователя.

# profiles.yml
jaffle_shop:
  target: dev
  outputs:
    dev:
      type: duckdb
      path: dev.duckdb
      schema: "dbt_{{ env_var('USER', 'dev') }}"

В dbt_project.yml:

models:
  jaffle_shop:
    +schema: ''  # без дополнительного префикса

Что произойдёт:

  • Alice запускает dbt run -> схема dbt_alice.
  • Bob запускает dbt run -> схема dbt_bob.
  • В прod target -> схема dbt_prod.

Каждый работает в своей изолированной схеме. Нет конфликтов.

custom schema через generate_schema_name

dbt позволяет переопределить логику генерации имён схем через macro generate_schema_name:

-- macros/generate_schema_name.sql

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- set default_schema = target.schema -%}
    
    {%- if target.name == 'prod' -%}
        {# В prod: target.schema + custom (если указано в config) #}
        {%- if custom_schema_name is none -%}
            {{ default_schema }}
        {%- else -%}
            {{ custom_schema_name | trim }}
        {%- endif -%}
    {%- else -%}
        {# В dev: всё в одну dev-схему, игнорируем custom #}
        {{ default_schema }}
    {%- endif -%}
{%- endmacro %}

Это самый частый pattern overrides в реальных проектах. Объяснение:

  • В prod: модель с config(schema='marketing') попадёт в marketing (или dbt_prod_marketing — зависит от логики).
  • В dev: ВСЕ модели попадают в dbt_alice, custom schema игнорируется.

Без этого в dev получите 10 разных схем (по числу custom-конфигов), и нужно помнить, что куда смотреть.

Паттерн 3: разное materialization для dev/prod

Table-материализация в dev замедляет итерации (после каждого dbt run пересоздание таблицы).

{{ config(
    materialized=('view' if target.name == 'dev' else 'table')
) }}

select * from ...

В dev — view (быстро). В prod — table (быстрый downstream запрос).

TIP

Это работает для всех materializations. Например, в dev incremental может быть table (с --full-refresh каждый раз), в prod — incremental. Зависит от кейса.

Паттерн 4: явный target check в hooks

В dbt_project.yml:

on-run-end:
  - "{% if target.name == 'prod' %}grant select on all tables in schema {{ target.schema }} to looker_role{% endif %}"

Hook выполнится только в prod. В dev grants не нужны (вы один работаете в своей схеме).

Полный пример: production-ready конфиг проекта

# dbt_project.yml

name: 'jaffle_shop'
version: '1.0.0'
profile: 'jaffle_shop'

vars:
  # Default — для prod
  lookback_days: 365
  include_pii: false
  enable_experimental_models: false

models:
  jaffle_shop:
    staging:
      +materialized: view
    intermediate:
      +materialized: ephemeral
    marts:
      +materialized: table
    
    # Конфигурация для dev
    +on-schema-change: append_new_columns

on-run-start:
  - "{% do log('Running in ' ~ target.name ~ ' target', info=true) %}"
# profiles.yml

jaffle_shop:
  target: dev
  outputs:
    dev:
      type: duckdb
      path: "{{ env_var('DBT_DUCKDB_PATH', 'dev.duckdb') }}"
      schema: "dbt_{{ env_var('USER', 'dev') }}"
      threads: 4
    
    prod:
      type: postgres
      host: "{{ env_var('PROD_PG_HOST') }}"
      user: "{{ env_var('PROD_PG_USER') }}"
      password: "{{ env_var('PROD_PG_PASS') }}"
      port: "{{ env_var('PROD_PG_PORT', '5432') | int }}"
      dbname: analytics
      schema: dbt_prod
      threads: 8
-- models/staging/stg_jaffle__orders.sql
{{ config(
    materialized='view'
) }}

select *
from {{ source('jaffle', 'raw_orders') }}

{% if target.name == 'dev' %}
where order_date >= current_date - interval '{{ var("lookback_days", 7) }}' day
{% else %}
where order_date >= current_date - interval '{{ var("lookback_days", 365) }}' day
{% endif %}
-- macros/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- set default_schema = target.schema -%}
    {%- if target.name == 'prod' and custom_schema_name is not none -%}
        {{ custom_schema_name | trim }}
    {%- else -%}
        {{ default_schema }}
    {%- endif -%}
{%- endmacro %}

Запуски:

# Разработка: 7 дней, схема dbt_alice
$ export USER=alice
$ dbt run --target dev

# Тот же код, но 30 дней:
$ dbt run --target dev --vars '{lookback_days: 30}'

# CI: отдельная схема, 90 дней
$ DBT_TARGET=ci dbt run --vars '{lookback_days: 90}'

# Prod: полная история, схема dbt_prod
$ dbt run --target prod

Дополнительные паттерны

Заводить только нужные модели в dev

Через --select и tags:

# В _models.yml
models:
  - name: heavy_aggregation_model
    config:
      tags: ['heavy']
# Dev: всё, кроме heavy
$ dbt run --target dev --exclude tag:heavy

# Prod: всё
$ dbt run --target prod

dev-only test severity

В dev тесты как warning, в prod — error:

columns:
  - name: total_amount
    data_tests:
      - not_null:
          config:
            severity: "{{ 'warn' if target.name == 'dev' else 'error' }}"

В dev разработчик может пропустить failing test (warn) и продолжить экспериментировать. В prod — error блокирует.

Отключение моделей в dev/prod

{{ config(
    enabled=(target.name == 'prod')
) }}

-- Эта модель собирается ТОЛЬКО в prod

Полезно для прод-only моделей: например, экспорт в external storage (parquet), который dev не нужен.

Распространённые ошибки

1. Забыли default для prod в {% if target.name == 'dev' %}.

{# Только dev обрабатывает, prod падает на parse #}
{% if target.name == 'dev' %}
    where order_date >= current_date - 7
{% endif %}

{# Если в prod не нужен фильтр — OK, ничего не выводится. Но если нужен — обязательно else #}

2. Хардкод схемы в SQL.

{# Bad: хардкод #}
from analytics.raw_orders

{# Good: через source/ref #}
from {{ source('jaffle', 'raw_orders') }}

Хардкод сломается, когда схема меняется между dev и prod.

3. target.name in [...] с одним target.

{% if target.name in ['dev', 'ci'] %}
    -- одинаковая логика для двух targets
{% endif %}

Чище, чем два {% if %} блока.

4. Запутаться, где target.schema, где var(‘schema_prefix’).

target.schema — из profiles.yml. var() — из dbt_project.yml. Не путать.

Попробуй сам

Соберите production-ready setup своего проекта:

  1. profiles.yml с двумя targets (dev, prod). Dev — DuckDB локально, prod — DuckDB на другом пути (имитация prod).
  2. dev схема включает $USER через env_var.
  3. Staging-модель фильтрует на 7 дней в dev и не фильтрует в prod.
  4. Mart-модель в dev — view, в prod — table.
  5. Hook в on-run-start, печатающий target.name.

Запустите оба:

$ dbt run --target dev
$ dbt run --target prod

Проверьте через DuckDB CLI, что в dbt_$USER схеме — 7 дней данных и views, а в dbt_prod — полная история и tables.

Проверка знанийKnowledge check
Что произойдёт, если в модели есть `{% if target.name == 'prod' %}`...`{% endif %}` без else, и модель запущена в dev?
ОтветAnswer
Блок внутри if пропускается. На output ничего не выводится — то, что было внутри if, не попадает в скомпилированный SQL. Пример: select * from ''{{ ref('orders') }}'' ''{% if target.name == 'prod' %}'' where order_date не меньше '2020-01-01' ''{% endif %}'' В dev: select * from "jaffle_shop"."dbt_alice"."orders" В prod: select * from "analytics"."dbt_prod"."orders" where order_date не меньше '2020-01-01' Это валидный SQL в обоих случаях — DuckDB или Postgres примут select без WHERE. Но: если у вас обратный сценарий ('в dev фильтруем, в prod нет'), то ДЕВЕЛОПЕР должен помнить, что в prod ничего не фильтруется. Иначе можно загрузить много данных: - Dev: where order_date не меньше 7 days ago — OK. - Prod: NO WHERE — может прочитать миллиарды записей. Лучше явно объявлять обе ветки через if/else: ''{% if target.name == 'prod' %}'' where order_date не меньше '2020-01-01' ''{% else %}'' where order_date не меньше current_date - 7 ''{% endif %}'' Так компилируется ОДИН WHERE в каждом случае, и нет недоразумений.
Проверка знанийKnowledge check
Зачем переопределять generate_schema_name macro в проекте, и какая частая боль это решает?
ОтветAnswer
Дефолтная логика dbt: если у модели есть config(schema='marketing'), она попадает в схему 'dbt_dev_marketing' (target.schema_custom_schema). В prod — 'dbt_prod_marketing'. Боль: в dev у каждого разработчика получаются ДЕСЯТКИ разных схем — dbt_alice_marketing, dbt_alice_finance, dbt_alice_default, и т.п. Сложно запомнить, где какая модель, особенно в IDE / dbt-docs. Override generate_schema_name решает это так: - В dev: ВСЕ модели в одной схеме (dbt_alice), custom_schema игнорируется. Все таблицы рядом, легко искать. - В prod: custom_schema применяется как есть (marketing, finance), без префикса target.schema. Получаем чистую структуру (marketing.fct_orders, finance.fct_revenue), которая видна BI-tools. Альтернативный override: - В prod: возвращаем custom_schema как есть. - В dev: возвращаем target.schema (типа dbt_alice) для всех. Это самый частый паттерн в реальных dbt-проектах. Без override команда новичков теряется в дев-схемах, prod-таблицы сложно соответствовать ожиданиям BI/AdHoc запросов. Macro в макрос-папке: macros/generate_schema_name.sql. dbt автоматически использует переопределение (override built-in macros — стандартный механизм dispatch).

Итоги

  • target — именованная конфигурация подключения. Объявляется в profiles.yml, переключается через --target или env var DBT_TARGET.
  • target.name, target.schema, target.type, target.threads — атрибуты текущего target. Используются в Jinja для условной логики.
  • Главные паттерны dev vs prod:
    • Filter в dev на N дней: {% if target.name == 'dev' %} where date >= 7 days ago {% endif %}.
    • Per-developer схемы: schema: "dbt_{{ env_var('USER', 'dev') }}".
    • Override generate_schema_name: в dev игнорировать custom schema, в prod — применять.
    • Materialization по target: dev — view (быстро), prod — table (производительность downstream).
  • Дополнительные: dev-only tags, conditional hooks, severity по target.
  • Антипаттерны: хардкод схем, забыть else, путаница var vs target.schema.

Этот паттерн — основа production-ready dbt-проекта. С ним вы развиваете без боли в dev, безопасно деплоите в prod. Курс dbt I завершён. Дальше — dbt II (middle): продвинутые темы, performance, complex incremental, CI/CD, и реальный production opsec.

target.name в production: conditional logic per environment на реальных кейсах GitHub Flow — workflow, в котором dbt-модели проходят dev -> prod

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 7. Что такое target в dbt?

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

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

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

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