Learning Platform
Глоссарий Troubleshooting
Урок 12.02 · 18 мин
Начальный
dbtmacrosdispatchpackages

Использование macros в моделях

В прошлом уроке мы научились объявлять macros. Теперь — как их вызывать из моделей, и что происходит, когда macros живут в разных пакетах. Разберём typical patterns, namespace resolution и встроенный механизм dispatch, который делает macros переносимыми между warehouse’ами.

Базовый вызов из модели

Macro вызывается через {{ }} — как обычный expression:

-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(col) %}
    ({{ col }} / 100.0)::numeric(16, 2)
{% endmacro %}
-- models/marts/marts__orders.sql
select
    order_id,
    {{ cents_to_dollars('total_amount_cents') }} as total_amount,
    {{ cents_to_dollars('tax_amount_cents') }} as tax_amount
from {{ ref('stg_jaffle__orders') }}

После компиляции:

select
    order_id,
    (total_amount_cents / 100.0)::numeric(16, 2) as total_amount,
    (tax_amount_cents / 100.0)::numeric(16, 2) as tax_amount
from "jaffle_shop"."main"."stg_jaffle__orders"

Это implicit return: текст из тела macro подставляется в место вызова.

Namespace: проектные macros vs пакетные

Когда вы устанавливаете dbt_utils через packages.yml, его macros становятся доступны под префиксом имени пакета:

-- Вызов macros проекта — БЕЗ префикса
{{ cents_to_dollars('amount_cents') }}

-- Вызов macros из dbt_utils — С префиксом пакета
{{ dbt_utils.surrogate_key(['order_id', 'customer_id']) }}
{{ dbt_utils.pivot('status', dbt_utils.get_column_values(ref('orders'), 'status')) }}

Префикс dbt_utils. — это namespace. Он позволяет dbt различать одноимённые macros в разных пакетах. Без префикса dbt ищет macro только в текущем проекте; с префиксом — в указанном пакете.

NOTE

Если в вашем проекте есть macro pivot, и в dbt_utils тоже есть pivot, без префикса будет вызван локальный. Это полезный механизм override: вы можете переопределить пакетный macro в своём проекте.

Поиск macro: алгоритм dbt

Встречен вызов macro_name(args) в моделиdbt видит ссылку на macro в expression-блоке, начинает резолвить
Префикс пакета указан?dbt_utils.macro_name или просто macro_name?
Если ДА — ищет в этом пакетеdbt_packages/dbt_utils/macros/ — рекурсивно
Если НЕТ — ищет в проектеmacros/ в корне текущего проекта
Найдено? -> запускаетПодставляет args, рендерит тело, возвращает результат
Не найдено -> ошибка'macro X not found' на этапе parse — фейл компиляции

Это объясняет частую ошибку: «macro X not found». Если забыли префикс пакета — dbt ищет в проекте, не находит, падает.

Реальные паттерны использования

Pattern 1: SQL-фрагмент с параметрами

Самый частый. Macro генерирует кусок SQL, который вы подставляете в модель.

-- macros/string_cleanup.sql
{% macro clean_email(col) %}
    nullif(trim(lower({{ col }})), '')
{% endmacro %}
-- models/staging/stg_jaffle__customers.sql
select
    customer_id,
    {{ clean_email('email') }} as email,
    {{ clean_email('alt_email') }} as alt_email
from {{ source('jaffle', 'raw_customers') }}

После compile:

select
    customer_id,
    nullif(trim(lower(email)), '') as email,
    nullif(trim(lower(alt_email)), '') as alt_email
from "jaffle_shop"."main"."raw_customers"

Pattern 2: surrogate key generation

dbt_utils.surrogate_key строит уникальный hash из нескольких колонок. Часто используется для PK в marts.

-- models/marts/marts__orders_v2.sql
select
    {{ dbt_utils.surrogate_key(['order_id', 'order_date', 'customer_id']) }} as order_key,
    order_id,
    customer_id,
    order_date,
    total_amount
from {{ ref('stg_jaffle__orders') }}

Compile:

select
    md5(cast(coalesce(cast(order_id as varchar), '_dbt_utils_surrogate_key_null_') || '-' || 
        coalesce(cast(order_date as varchar), '_dbt_utils_surrogate_key_null_') || '-' || 
        coalesce(cast(customer_id as varchar), '_dbt_utils_surrogate_key_null_') as varchar)) as order_key,
    order_id, customer_id, order_date, total_amount
from "jaffle_shop"."main"."stg_jaffle__orders"

Macro обрабатывает NULLs, casting, конкатенацию — вам не нужно помнить SQL-нюансы.

Pattern 3: dynamic column list

-- models/marts/marts__customer_pivot.sql
{% set statuses = dbt_utils.get_column_values(
    table=ref('stg_jaffle__orders'),
    column='status'
) %}

select
    customer_id,
    {% for s in statuses %}
    sum(case when status = '{{ s }}' then 1 else 0 end) as count_{{ s }}
    {% if not loop.last %},{% endif %}
    {% endfor %}
from {{ ref('stg_jaffle__orders') }}
group by customer_id

dbt_utils.get_column_values возвращает список — explicit return. Используем в for.

Pattern 4: применить macro в каждой staging-модели

Допустим, во всех staging-моделях нужно нормализовать страны через clean_country(). Чтобы не повторять везде, выносим в macro и вызываем:

-- macros/clean_country.sql
{% macro clean_country(col) %}
    case 
        when upper({{ col }}) in ('US', 'USA', 'UNITED STATES') then 'US'
        when upper({{ col }}) in ('GB', 'UK', 'UNITED KINGDOM') then 'GB'
        when upper({{ col }}) in ('DE', 'GERMANY', 'DEUTSCHLAND') then 'DE'
        else upper({{ col }})
    end
{% endmacro %}
-- В каждой staging-модели:
select 
    customer_id,
    {{ clean_country('country') }} as country_code
from ...

Через год бизнес добавляет «обрабатывать FR, JP, BR» — один edit в macro обновляет все staging-модели.

Dispatch: один macro, разные адаптеры

Реальная боль: SQL-синтаксис различается между warehouse’ами. current_date - interval 7 day в DuckDB; dateadd(day, -7, current_date) в SQL Server; разные кавычки и квоты везде.

dbt решает это через adapter dispatch: один публичный macro, под капотом — разные реализации для адаптеров.

Анатомия:

-- macros/date_helpers.sql

-- Публичный macro — точка входа
{% macro date_subtract(days) %}
    {{ return(adapter.dispatch('date_subtract')(days)) }}
{% endmacro %}

-- Default реализация (PostgreSQL-стиль) — берётся, если нет специфичной для адаптера
{% macro default__date_subtract(days) %}
    current_date - interval '{{ days }}' day
{% endmacro %}

-- BigQuery-специфичная
{% macro bigquery__date_subtract(days) %}
    date_sub(current_date, interval {{ days }} day)
{% endmacro %}

-- SQL Server-специфичная
{% macro sqlserver__date_subtract(days) %}
    dateadd(day, -{{ days }}, getdate())
{% endmacro %}

В модели вы вызываете только публичный macro:

select * from {{ ref('orders') }}
where order_date >= {{ date_subtract(7) }}

dbt сам выбирает правильную реализацию в зависимости от target.type:

  • DuckDB / Postgres -> default__date_subtract -> current_date - interval '7' day
  • BigQuery -> bigquery__date_subtract -> date_sub(...)
  • SQL Server -> sqlserver__date_subtract -> dateadd(...)
TIP

Junior обычно НЕ пишет dispatch сам — это middle/senior уровень. Но понимать, как работает, нужно: половина macros в dbt_utils использует dispatch, и вам предстоит читать чужой код.

Override пакетного macro

Иногда нужно подменить реализацию macro из пакета — например, dbt_utils.surrogate_key, чтобы использовать sha256 вместо md5. Делается через объявление в своём проекте с тем же именем:

-- macros/surrogate_key.sql (в вашем проекте)
{% macro generate_surrogate_key(field_list) %}
    sha256(concat({{ field_list | join(', ') }}))
{% endmacro %}

Если в dbt_project.yml есть:

dispatch:
  - macro_namespace: dbt_utils
    search_order: ['my_project', 'dbt_utils']

То при вызове dbt_utils.generate_surrogate_key dbt сначала ищет в my_project. Если найдёт — использует. Это dispatch search order.

Для junior это полезно знать как способ кастомизации без форка пакета. Подробно — в курсе dbt II.

Вызов macro вне моделей

Macros вызываются не только из моделей, но и из:

Hooks (on-run-start, on-run-end)

# dbt_project.yml
on-run-start: "{{ create_audit_log_table() }}"
on-run-end: "{{ log_run_summary() }}"

Hook исполняется до или после dbt run. Внутри — стандартный macro call.

dbt run-operation

Можно вызывать macro прямо из CLI:

$ dbt run-operation backfill_partition --args '{date: 2026-05-19}'

Это запустит macro backfill_partition(date='2026-05-19'). Используется для админ-задач: backfill, очистка, миграции схем.

-- macros/backfill_partition.sql
{% macro backfill_partition(date) %}
    {% set sql %}
        delete from {{ ref('orders') }} where order_date = '{{ date }}';
        insert into {{ ref('orders') }} ...
    {% endset %}
    {% do run_query(sql) %}
    {% do log("Backfilled " ~ date, info=True) %}
{% endmacro %}

Tests

В прошлом модуле вы видели, что generic tests — это macros особого вида ({% test %} ... {% endtest %}).

Models config

В YAML _models.yml можно вызывать macros в выражениях:

models:
  - name: orders
    config:
      tags: "{{ env_var('TENANT_ID') }}"

Macros — универсальный механизм dbt, не привязанный только к моделям.

Хорошие практики

1. Используй вариант с префиксом пакета явно. {{ dbt_utils.surrogate_key(...) }} лучше, чем надеяться на namespace resolution. Читателю кода сразу понятно, откуда macro.

2. Не вызывай больше 1-2 macros на колонку. Если строка превратилась в {{ a({{ b({{ c('col') }}) }}) }} — это нечитаемо. Лучше один комплексный macro cleanup_email, чем три каскадных.

3. Macro = функция, у которой одна задача. clean_email хорошо. process_customer_data (который делает 5 вещей) — плохо.

4. Документируй пакетные macros в README. Когда команда вырастет, README в macros/ с примерами вызова бесценен.

5. Не злоупотребляй custom macros. Сначала проверь, нет ли готового в dbt_utils. Своё писать — только когда нет аналога.

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

1. Macro не найден.

Compilation Error
  In model 'my_model' macro 'cents_to_dollars' could not be found.

Проверь:

  • Файл лежит в macros/.
  • Внутри файла {% macro cents_to_dollars(...) %} (а не cent_to_dollar или с опечаткой).
  • Если из пакета — добавил префикс (dbt_utils.X).
  • dbt deps запущен после изменения packages.yml.

2. Macro вернул не то.

-- Жду список колонок, получаю строку '['a', 'b', 'c']'
{% set cols = get_columns() %}
{% for c in cols %}...{% endfor %}  -- упадёт, потому что строка не итерируется ОК

Скорее всего, в macro забыли {% do return(...) %}. Тело генерирует output, и присваивание получает строковое представление.

3. Передача аргументов через позиции, когда нужно по имени.

-- macros/foo.sql
{% macro foo(a, b, c=10) %}
    {{ a + b + c }}
{% endmacro %}

-- Вызов
{{ foo(1, c=20, b=2) }}  -- работает
{{ foo(1, b=2, c=20) }}  -- работает
{{ foo(c=20, 1, 2) }}    -- ошибка: keyword перед positional

Правило Python: позиционные раньше keyword.

Попробуй сам

В dbt_utils есть macro generate_surrogate_key (новое имя surrogate_key deprecated с 1.3). Используйте его для генерации PK в mart-модели:

-- models/marts/marts__orders.sql
select
    {{ dbt_utils.generate_surrogate_key(['order_id', 'order_date']) }} as order_sk,
    order_id,
    order_date,
    customer_id,
    total_amount
from {{ ref('stg_jaffle__orders') }}

Запустите dbt compile и посмотрите, какой SQL получился в target/compiled/. Затем в DuckDB CLI убедитесь, что order_sk действительно уникален и стабилен.

Проверка знанийKnowledge check
В вашем проекте определён macro 'clean_email' в macros/clean_email.sql. В dbt_utils тоже есть macro 'clean_email'. Что произойдёт при вызове `{{ clean_email('field') }}` БЕЗ префикса пакета, и как изменить поведение?
ОтветAnswer
Без префикса пакета dbt ищет macro ТОЛЬКО в текущем проекте — найдёт ваш clean_email и использует его, dbt_utils.clean_email будет проигнорирован. Это не баг, а фича: разрешение конфликтов имён по умолчанию favours local. Если хотите вызвать именно dbt_utils — указывайте префикс: ''{{ dbt_utils.clean_email('field') }}''. Можно изменить порядок через dispatch search order в dbt_project.yml: dispatch: - macro_namespace: dbt_utils search_order: ['dbt_utils', 'my_project'] Тогда при вызове dbt_utils.clean_email dbt сначала проверит dbt_utils, потом fallback на my_project. Это редкий случай — обычно local-first порядок правильный, потому что override packaged macros в проекте — стандартный паттерн (например, чтобы заменить md5 на sha256 в surrogate_key). Best practice: явно префиксовать packaged macros в коде (''{{ dbt_utils.X }}''), чтобы избежать сюрпризов от namespace shadowing.
Проверка знанийKnowledge check
Что такое adapter dispatch и как dbt выбирает реализацию macro между default__name, postgres__name, bigquery__name?
ОтветAnswer
Adapter dispatch — механизм, позволяющий иметь ОДИН публичный macro с разными warehouse-специфичными реализациями. Структура: 1. Публичный macro (без префикса): ''{% macro name(args) %}'' ''{{ return(adapter.dispatch('name')(args)) }}'' ''{% endmacro %}'' 2. Реализации с префиксом адаптера: default__name, postgres__name, bigquery__name, sqlserver__name, и т.д. При вызове adapter.dispatch dbt: 1. Определяет тип текущего target (target.type — duckdb, postgres, bigquery, ...). 2. Ищет macro с префиксом этого адаптера: {target.type}__name. Например, для DuckDB — duckdb__name. 3. Если не находит — fallback на default__name. 4. Если и default__ нет — ошибка 'no implementation found'. DuckDB обычно использует default__ реализации (синтаксис близок к PostgreSQL). BigQuery, Snowflake, SQL Server переопределяют те macros, где синтаксис отличается. Для junior правило: не пишешь dispatch с нуля, но знаешь, как читать. Если в чужом коде видишь ''{{ return(adapter.dispatch('X')(args)) }}'' — ищи реализации с префиксами в том же или соседнем файле. Junior реальный кейс: использовать dispatch-aware macros из dbt_utils без понимания внутренностей — это нормально. Реализация dispatch — middle/senior уровень.

Итоги

  • Macro вызывается из модели через {{ macro_name(args) }} — как обычный expression.
  • Macros проекта вызываются без префикса; пакетные — с префиксом (dbt_utils.X).
  • dbt ищет macro: с префиксом — в пакете, без префикса — в проекте. Local-first.
  • Adapter dispatch (adapter.dispatch) — механизм для разных warehouse-реализаций. Не пишем сами на junior, но читаем чужой код.
  • Override packaged macros — через одноимённый macro в проекте, опционально с настройкой dispatch.search_order.
  • Macros используются не только в моделях: hooks, run-operation, tests, YAML configs.
  • Реальные паттерны: SQL-фрагменты, surrogate keys, dynamic column lists, нормализация значений.
adapter.dispatch: multi-warehouse макросы

В следующем уроке — тур по самым полезным macros из dbt_utils: star, pivot, generate_surrogate_key, date_spine и другие, которые сэкономят сотни строк кода.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 7. Чтобы вызвать macro 'cents_to_dollars' из пакета dbt_utils, как написать в модели?

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

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

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

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