Использование 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 только в текущем проекте; с префиксом — в указанном пакете.
Если в вашем проекте есть macro pivot, и в dbt_utils тоже есть pivot, без префикса будет вызван локальный. Это полезный механизм override: вы можете переопределить пакетный macro в своём проекте.
Поиск macro: алгоритм dbt
Это объясняет частую ошибку: «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(...)
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 действительно уникален и стабилен.
Итоги
- 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, нормализация значений.
В следующем уроке — тур по самым полезным macros из dbt_utils: star, pivot, generate_surrogate_key, date_spine и другие, которые сэкономят сотни строк кода.