Разбираем макросы в SQL: Jinja, нативные макросы и AST-подход. Узнайте, как избежать пяти вариантов кода и сделать пересборку моделей корректной. Читайте и применяйте!
Если вы работали с dbt, то знаете: макросы в Jinja — это пять вариантов одного кода для разных диалектов. Но есть способ проще. В этой статье разберём, как макросы работают в Jinja, в DuckDB и в новом инструменте Interlace, который расширяет их на уровне AST. Вы узнаете, как сократить дублирование и сделать пересборку моделей предсказуемой.
Макрос — это именованное выражение с параметром. Например, cents_to_dollars — это просто деление на 100 с приведением к decimal. В Jinja макрос — это текст, который подставляется до парсинга SQL. Поэтому для каждого диалекта нужен свой вариант: Postgres требует каст перед делением, BigQuery — тип NUMERIC и round.
В DuckDB есть нативный синтаксис:
-- macros/money.sql
CREATE MACRO cents_to_dollars(amount) AS (amount / 100)::numeric(16, 2);Теперь можно вызывать макрос в любой модели:
-- models/stg_orders.sql
SELECT order_id,
cents_to_dollars(subtotal) AS subtotal,
cents_to_dollars(tax_paid) AS tax_paid
FROM raw_ordersМакрос должен быть заменён своим телом в один из трёх моментов. От этого зависит всё.
Подстановка происходит до понимания SQL, поэтому для разных движков нужны разные тексты — отсюда пять вариантов. Вы теряете дерево запроса: после рендера структура исчезает.
DuckDB и Postgres поддерживают нативные макросы. Регистрируете макрос один раз, и все модели его вызывают. Но это ломает инвалидацию: если изменить макрос, модели не пересоберутся, потому что их SQL не изменился. Это тихая ошибка, которую обнаружат через три недели.
Interlace разворачивает макрос после парсинга, но до вычисления отпечатка и построения графа зависимостей. Это даёт все преимущества: один макрос для всех диалектов, корректная пересборка и видимость в lineage.
Поскольку разворачивание даёт AST, а не текст, транслятор (sqlglot) сам заботится о диалектах. Для макроса cents_to_dollars:
CAST((subtotal / 100) AS DECIMAL(16, 2))CAST((CAST(subtotal AS DOUBLE PRECISION) / NULLIF(100, 0)) AS DECIMAL(16, 2))CAST((subtotal / NULLIF(100, 0)) AS DECIMAL(16, 2))CAST((subtotal / NULLIF(100, 0)) AS NUMERIC)Вам не нужно писать postgres__cents_to_dollars — sqlglot знает особенности диалектов. Это работает для обычных случаев; специфичные вещи всё равно потребуют внимания.
Так как разворачивание включено в канонический SQL, отпечаток модели учитывает тело макроса. Изменили 100 на 1000 — и plan показывает пересборку всех моделей, которые вызывают макрос или зависят от них. Не нужно ничего декларировать — граф зависимостей работает сам.
Когда макрос разворачивается, lineage видит все источники данных, даже те, что не упомянуты в вызове. Например, для макроса net(x) AS x - shipping_fee lineage покажет net_total ← raw_orders.subtotal, raw_orders.shipping_fee. Если оставить макрос как чёрный ящик, shipping_fee исчезнет из графа — и это сломает анализ влияния.
Аналогично, если макрос ссылается на модель (например, in_gbp использует fx_rates), все вызывающие его модели получат зависимость от fx_rates автоматически.
macros/*.sql, путь настраивается через macro_paths.Макрос не существует в хранилище. Нельзя вызвать cents_to_dollars в ad-hoc запросе. Если вам это критично, используйте нативный CREATE MACRO — но тогда вы сами отвечаете за инвалидацию.
Попробуйте Interlace: pip install interlaced. Посмотрите пример examples/jaffle-shop — там макросы используются для случаев из демо-проекта dbt. Вы увидите, как один макрос заменяет пять и как пересборка становится корректной. Начните с простого макроса в своём проекте и оцените разницу.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →