Learning Platform
Глоссарий Troubleshooting
Урок 12.01 · 22 мин
Средний
Multi-environmenttargetJinjaConditional logicProduction patterns

target.name: conditional logic в Jinja per environment

dbt-проекты редко живут в одном environment. Типично: developer работает локально (dev), CI как окружение запускает dbt (ci) — механика CI/CD в модулях 12-13, scheduled runs в production (prod). У каждого environment могут быть свои отличия: разная схема (dev -> personal, prod -> clean), разная sample size (dev -> 1000 rows, prod -> all), разная materialization config (dev -> view для быстрого reload, prod -> table для performance).

dbt-core предоставляет target object — runtime context Jinja с info про текущий environment. target.name, target.database, target.schema, target.threads — это data, доступные в SQL моделях, YAML configs, macros. Через {% if target.name == 'prod' %} можно реализовать conditional logic.

Этот урок — про target object, common patterns multi-env, и антипаттерны hard-coded values.

dbt-iii: parse vs execute — когда Jinja резолвит target

Что такое target

target — это runtime info про currently executing dbt run. Defined в profiles.yml:

# ~/.dbt/profiles.yml
my_project:
  target: dev          # default target

  outputs:
    dev:
      type: duckdb
      path: './analytics_dev.duckdb'
      schema: 'dbt_levo_dev'
      threads: 4

    ci:
      type: duckdb
      path: ':memory:'
      schema: 'main'
      threads: 4

    prod:
      type: duckdb
      path: '/data/analytics_prod.duckdb'
      schema: 'analytics'
      threads: 16

При запуске:

dbt run                        # target = dev (from default)
dbt run --target ci            # target = ci
dbt run --target prod          # target = prod

В Jinja доступен target object с полями:

ПолеЗначение
target.nameИмя target (dev/ci/prod)
target.typeAdapter type (duckdb/snowflake/postgres)
target.databaseИмя database
target.schemaSchema (custom_schema если override)
target.threadsConcurrent threads
target.userUsername (per profile)
target.hostHost (для networked DBs)
target.portPort

Также adapter-specific поля (например, для Snowflake target.warehouse, target.role).


Базовое использование

Pattern 1: conditional filter

В dev — limit rows для быстрого dev cycle, в prod — full data:

SELECT * FROM {{ ref('large_source') }}
{% if target.name == 'dev' %}
  LIMIT 1000   -- dev: sample
{% endif %}

dbt run локально:

LIMIT 1000 applied

dbt run --target prod:

No LIMIT (full data)

Pattern 2: conditional materialization

В dev — view (быстрый reload), в prod — table (performance):

- name: customer_metrics
  config:
    materialized: "{{ 'table' if target.name == 'prod' else 'view' }}"

Pattern 3: conditional sources

{% set source_table = 'raw.orders_full' if target.name == 'prod' else 'raw.orders_sample' %}

SELECT * FROM {{ source_table }}

(Лучше через source(), но иногда нужно как пример.)

Pattern 4: conditional config через macro

- name: customer_metrics
  config:
    persist_docs:
      relation: "{{ target.name == 'prod' }}"
      columns: "{{ target.name == 'prod' }}"

В prod — persist docs (BI tools видят), в dev — нет (overhead).


target в SQL моделях

-- models/marts/customer_metrics.sql
SELECT
    c.customer_id,
    c.first_name,
    SUM(o.order_total) AS lifetime_value
FROM {{ ref('stg_customers') }} c
LEFT JOIN {{ ref('stg_orders') }} o ON o.customer_id = c.customer_id
{% if target.name == 'dev' %}
  WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 day'   -- dev: только recent
{% endif %}
GROUP BY c.customer_id, c.first_name

В dev fast (30 days of data), в prod full history.


target в YAML configs

# dbt_project.yml
models:
  my_project:
    +materialized: "{{ 'table' if target.name == 'prod' else 'view' }}"
    marts:
      +persist_docs:
        relation: "{{ target.name == 'prod' }}"
        columns: "{{ target.name == 'prod' }}"
      +schema: "marts"

Конфигурация условно applied per env.


Common patterns multi-env

Multi-env patterns

Полный пример: проект с 3 targets

# profiles.yml
my_project:
  target: dev

  outputs:
    dev:
      type: duckdb
      path: './dev.duckdb'
      schema: "dbt_{{ env_var('USER', 'unknown') }}_dev"
      threads: 4

    ci:
      type: duckdb
      path: ':memory:'
      schema: 'ci'
      threads: 4

    prod:
      type: duckdb
      path: '/data/prod.duckdb'
      schema: 'analytics'
      threads: 16

dbt_project.yml:

name: my_project
version: '1.0.0'

vars:
  # default vars
  reference_date: "{{ run_started_at.date() }}"
  sample_size: 1000

models:
  my_project:
    # default: table в prod, view везде
    +materialized: "{{ 'table' if target.name == 'prod' else 'view' }}"

    staging:
      +schema: stg
      +persist_docs:
        relation: false   # staging — no persist anywhere

    marts:
      +schema: marts
      +materialized: "{{ 'table' if target.name == 'prod' else 'view' }}"
      +persist_docs:
        relation: "{{ target.name == 'prod' }}"
        columns: "{{ target.name == 'prod' }}"

tests:
  # warn в dev, error в prod
  +severity: "{{ 'error' if target.name in ('prod', 'ci') else 'warn' }}"

Use:

dbt run                          # dev: views, no persist, warn tests
dbt run --target ci              # ci: in-memory, no persist, error tests
dbt run --target prod            # prod: tables, persist docs, error tests

Антипаттерны target

1. Hard-coded target.name

-- ПЛОХО: тест зависит от dev
WHERE order_date >= CURRENT_DATE - INTERVAL '30 day'   -- dev только

Без condition — продакшен фильтрует 30 days. Maybe wrong.

-- ХОРОШО: explicit conditional
{% if target.name == 'dev' %}
  WHERE order_date >= CURRENT_DATE - INTERVAL '30 day'
{% endif %}

2. target.name в data tests

# ПЛОХО
data_tests:
  - dbt_utils.expression_is_true:
      expression: "{% if target.name == 'dev' %} count > 100 {% else %} count > 10000 {% endif %}"

Сложная логика, hard to debug. Better — use vars или separate tests per env.

3. Too many target conditions

{% if target.name == 'dev' %}
  ...
{% elif target.name == 'ci' %}
  ...
{% elif target.name == 'staging' %}
  ...
{% elif target.name == 'prod' %}
  ...
{% endif %}

10 такого + код становится unreadable. Refactor — extract в macro:

{{ get_sample_filter() }}
{% macro get_sample_filter() %}
  {% if target.name in ('dev', 'ci') %}
    WHERE date >= CURRENT_DATE - INTERVAL '30 day'
  {% elif target.name == 'staging' %}
    WHERE date >= CURRENT_DATE - INTERVAL '90 day'
  {% else %}
    -- prod: no filter
  {% endif %}
{% endmacro %}

4. target.name для secrets

-- ПЛОХО — secrets в SQL
{% if target.name == 'prod' %}
  WHERE api_key = 'sk-prod-12345'
{% endif %}

Secrets — через env_vars (следующий урок), не conditional на target.

5. Forgotten dev-only conditions в commit

Developer добавил WHERE order_date >= ... для testing, forgot to wrap in {% if target.name == 'dev' %}. Production runs с filter — broken data.

Solution — code review + CI test что prod target gives expected results.

6. target в run-only macros

Some macros (run_query, statement) execute только на execute=True mode. Don’t rely on target.name выбор в parse-time logic without checking execute mode.


target patterns для CI

CI как окружение использует свой target: ci. Подробная механика CI пайплайнов, jobs, workflow — в модулях 12-ci-cd-local и 13-ci-cd-github. Здесь важно: CI target в profiles.yml существует наравне с dev/prod, и тот же conditional logic (target.name == 'ci') применим.


env_var в target

В profiles.yml можно использовать env_var для гибкости:

prod:
  type: duckdb
  path: "{{ env_var('DBT_PROD_DB_PATH', '/data/prod.duckdb') }}"
  threads: "{{ env_var('DBT_PROD_THREADS', 16) | int }}"

Это позволяет same profiles.yml для разных deploys (staging vs prod), различающихся через env vars. Об этом — следующий урок.


Тестирование conditional logic

Локально:

# Test dev behavior
dbt run --target dev --select customer_metrics
dbt show --select customer_metrics --limit 10

# Test prod behavior
dbt run --target prod --select customer_metrics
dbt show --select customer_metrics --limit 10

Проверить, что:

  • Dev filters work
  • Prod gets all data
  • No surprises после deploy

(Запуск нескольких targets в CI пайплайне — см. модуль 13-ci-cd-github.)


Попробуй сам

  1. Setup profiles.yml с dev/ci/prod targets.

  2. Add conditional:

    • В одной модели: LIMIT 1000 для dev, full для prod
    • В config: persist_docs только prod
    • В test: severity warn dev, error prod
  3. Test:

    dbt run --target dev
    dbt show --select my_model   # verify limit
    dbt run --target prod
    dbt show --select my_model   # verify no limit
  4. Try refactor if много conditions — extract в macro.

  5. Anti-pattern check — найти любую модель с hard-coded value depend on environment. Refactor с target.name.


Ключевые выводы

  1. target object в Jinja — runtime info про текущий dbt run. Fields: name, type, database, schema, threads, user, etc.
  2. Conditional logic через {% if target.name == 'prod' %} — для multi-env behavior (filters, materialization, persist_docs).
  3. Common patterns: sample data в dev, materialization optimization (table prod / view dev), persist_docs only prod, test severity по env.
  4. dbt_project.yml configs могут условно зависеть от target — materialized: "{{ 'table' if target.name == 'prod' else 'view' }}".
  5. Anti-patterns: hard-coded conditions (без if wrap), too many target conditions (refactor в macro), secrets через target.name (use env_var), forgotten dev-only conditions в commit.
  6. Refactor много conditions в macro: get_sample_filter() — DRY, testable.
  7. CI should mirror prod — materialized tables, full tests, error severity. Differences: smaller data, ephemeral storage.
  8. Тестирование conditional — dbt run --target X на каждом target во время разработки + CI runs всех targets.
Проверка знанийKnowledge check
Developer добавил в модель `WHERE order_date >= CURRENT_DATE - INTERVAL '30 day'` для testing локально, forgot to wrap в target.name conditional, merged. На проде revenue dashboard показывает только last month, не все time. Что случилось и как prevented?
ОтветAnswer
**Что случилось**:\n\nDeveloper testing локально needed faster runs — added WHERE filter для 30-day window. Code review approved — reviewer didn't catch missing target conditional. Production runs с filter — historical data исчезает из dashboards.\n\n**Production impact**:\n- Revenue dashboards теряют historical data\n- ML models trained на 30-day-only window\n- Reporting wrong (показывает 'revenue = $X for last month' вместо 'all-time')\n- Recovery: revert filter, --full-refresh\n\n**How prevent**:\n\n**1. Wrap conditional**:\n```sql\n{% if target.name == 'dev' %}\n WHERE order_date не меньше CURRENT_DATE - INTERVAL '30 day'\n{% endif %}\n```\n\nОтсутствие filter на prod — explicit.\n\n**2. Code review checklist**:\n- 'Any WHERE / LIMIT only in dev?' — yes/no\n- 'Wrapped в target conditional?' — yes/no\n- Reviewer must check before approval\n\n**3. CI gate** — production-like dry run:\n```yaml\n- name: Dry run with prod target\n run: dbt build --target ci-prod-like --vars '...'\n # CI ci-prod-like target mirrors prod settings (no filters)\n```\n\nЕсли row count drastically less than expected — alert.\n\n**4. Data tests на expected row count**:\n```yaml\n- name: revenue_daily\n data_tests:\n - dbt_utils.expression_is_true:\n expression: "COUNT(*) > 1000000" # > 1M rows expected\n config:\n severity: error\n```\n\nIf filter applied unexpectedly -> test fails.\n\n**5. Pre-commit hook**:\n```bash\n# Detect hardcoded WHERE filters\ngrep -rE "WHERE.*INTERVAL '[0-9]+ day'" models/ | grep -v "target.name" && exit 1\n```\n\nCatches at commit time.\n\n**6. Macro для consistency**:\n```sql\n{% macro dev_filter() %}\n {% if target.name == 'dev' %}\n WHERE order_date не меньше CURRENT_DATE - INTERVAL '30 day'\n {% endif %}\n{% endmacro %}\n```\n\nThen в model: `{{ dev_filter() }}` — explicit name shows intent.\n\n**Cultural**: код review с focus on multi-env safety. New engineer training mentions this anti-pattern.\n\nThis is **production engineering hygiene** — каждый WHERE / LIMIT нужно проверить: 'works correctly на prod target?'
Проверка знанийKnowledge check
Senior говорит: 'персистируй docs только в prod. Materialize tables только в prod. Use views в dev для speed.' Опиши implementation в dbt_project.yml.
ОтветAnswer
Conditional materialization + persist_docs в dbt_project.yml через target.name.\n\n**dbt_project.yml**:\n\n```yaml\nname: my_project\nversion: '1.0.0'\n\nmodels:\n my_project:\n # default для всех моделей: table в prod, view везде\n +materialized: "{{ 'table' if target.name == 'prod' else 'view' }}"\n\n staging:\n +schema: stg\n +persist_docs:\n relation: false # staging — no persist anywhere (overhead)\n columns: false\n\n intermediate:\n +schema: int\n # default materialization (table prod / view dev) applies\n +persist_docs:\n relation: false\n columns: false\n\n marts:\n +schema: marts\n +materialized: "{{ 'table' if target.name == 'prod' else 'view' }}"\n +persist_docs:\n relation: "{{ target.name == 'prod' }}"\n columns: "{{ target.name == 'prod' }}"\n```\n\n**Что happens на каждом target**:\n\n**Dev** (`dbt run --target dev`):\n- Materialized: view (быстрый rebuild при изменении SQL)\n- Persist docs: false (no warehouse comments)\n- Fast iteration loop для developer\n\n**CI** (`dbt run --target ci`):\n- Materialized: view (or table depending on configuration choice)\n- Persist docs: false (no need в ephemeral CI)\n- Fast — focused on testing\n\n**Prod** (`dbt run --target prod`):\n- Materialized: table (query performance)\n- Persist docs: true (BI tools видят descriptions)\n- Production-optimized\n\n**Why these choices**:\n\n**Materialization**:\n- **Dev = view**: developer changes SQL frequently, view rebuilds на каждый ref read. No physical storage cost.\n- **Prod = table**: dashboards query frequently, table читается быстро. Storage cost worth performance.\n\n**Persist docs**:\n- **Dev = false**: developer iterates fast, не нужны warehouse comments (use dbt docs locally instead). Avoid DDL overhead.\n- **Prod = true**: BI tools (Tableau, DataGrip) read warehouse metadata. Descriptions provide context to analysts.\n\n**Edge cases**:\n\n**1. Slow dev rebuild**:\nIf views too slow для big tables в dev -> use `materialized: ephemeral` (CTE inlining) или add LIMIT для sample.\n\n**2. CI needs production-like**:\nFor pre-prod testing — separate CI target c `materialized: table` (mirror prod). CI можно run с both prod-like и dev-like targets.\n\n**3. Incremental models**:\n```yaml\nmodels:\n my_project:\n marts:\n large_facts:\n +materialized: "{{ 'incremental' if target.name == 'prod' else 'table' }}"\n```\nDev incrementals — pain (need to track --full-refresh state). Use table в dev для simplicity. Prod incremental для performance.\n\n**Custom per-model overrides** still possible:\n\n```yaml\n- name: very_large_mart\n config:\n materialized: incremental # override default\n unique_key: id\n```\n\nЭто **production engineering setup** — env-aware defaults, per-model overrides где нужно. Clear separation dev (speed) vs prod (performance).

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Developer добавил `WHERE order_date >= CURRENT_DATE - INTERVAL '30 day'` в модель для testing локально. Forgot wrap в target.name conditional. Merged. Production breaks. Что implement?

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

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

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

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