Jinja: expressions, statements, comments
До сих пор мы использовали Jinja почти неявно: {{ ref('orders') }} в моделях, {{ source('jaffle', 'raw_orders') }} в staging. Это были конкретные функции — ref и source — без понимания, что в них общего. А общее это Jinja2 — шаблонизатор Python, на котором написан весь движок dbt. SQL-модели в dbt — это не SQL, это Jinja-шаблоны, которые после компиляции становятся SQL. Без понимания Jinja вы дальше staging-уровня не уедете.
Этот модуль — про Jinja. Цель — научиться читать любой dbt-шаблон, понимать, что произойдёт при компиляции, и писать собственные. Начнём с трёх базовых конструкций.
Три типа Jinja-блоков
Jinja2 различает три синтаксические конструкции:
Всё остальное в файле — обычный текст (в случае dbt — SQL), который Jinja не трогает.
Простой пример:
{# Это комментарий — не попадёт в скомпилированный SQL #}
{% set table_name = 'orders' %}
select *
from {{ ref(table_name) }}
where 1=1
После компиляции:
select *
from "jaffle_shop"."main"."orders"
where 1=1
Что произошло:
- Комментарий
{# ... #}удалён. {% set table_name = 'orders' %}выполнен, переменная создана, в output ничего не вывелось (statement не выводит).{{ ref(table_name) }}— expression: вычислено как строка"jaffle_shop"."main"."orders", подставлено вместо{{ }}.
Когда использовать каждое
Главное правило, которое путает junior: expression нельзя использовать как statement, и наоборот. Это разные синтаксические категории.
Что НЕ работает
{# Ошибка: пытаемся использовать выражение как statement #}
{{ set table_name = 'orders' }}
{# Ошибка: пытаемся выполнить statement как expression #}
{% ref('orders') %}
Правильно так:
{# set — это statement #}
{% set table_name = 'orders' %}
{# ref() — функция, возвращающая значение, нужен expression #}
{{ ref('orders') }}
Подробно: expressions
Expression возвращает значение, которое Jinja приводит к строке и вставляет в текст. Может быть:
- Переменная:
{{ table_name }} - Литерал:
{{ 'hello' }},{{ 42 }},{{ true }} - Функция:
{{ ref('orders') }},{{ source('jaffle', 'raw') }},{{ var('start_date') }} - Арифметика:
{{ 1 + 2 }}->3 - Сравнение:
{{ x > y }}->TrueилиFalse - Доступ к атрибутам:
{{ target.name }},{{ target.database }} - Filters (через
|):{{ name | upper }},{{ "hello" | length }}
Примеры в dbt-контексте:
-- {{ ref('orders') }} вернёт полный qualifier: "db"."schema"."orders"
select * from {{ ref('orders') }}
-- target.name вернёт имя текущего target (dev/prod)
select '{{ target.name }}' as env, * from foo
-- var с default
where order_date >= '{{ var("start_date", "2026-01-01") }}'
-- Filter | upper
select '{{ "production" | upper }}' as env
-- -> select 'PRODUCTION' as env
В курсе вы часто увидите '{{ target.name }}' — кавычки вокруг expression. Это потому, что Jinja подставляет значение как есть, без кавычек. Если нужна строка в SQL, кавычки добавляете вы. Для ref() и source() — нет, они возвращают уже квотированный qualifier.
Подробно: statements
Statement управляет генерацией SQL. Сам statement в output не попадает, но его эффекты (вывод внутри {% for %}, изменение переменной) — попадают.
Список самых частых:
| Statement | Что делает |
|---|---|
{% set var = value %} | Создаёт/обновляет переменную |
{% if cond %} ... {% endif %} | Условный блок |
{% if cond %} ... {% else %} ... {% endif %} | Условный блок с else |
{% for x in list %} ... {% endfor %} | Цикл |
{% macro name(args) %} ... {% endmacro %} | Определение macro |
{% include 'path' %} | Подключение другого шаблона |
Пример:
{% set columns = ['id', 'name', 'email'] %}
select
{% for col in columns %}
{{ col }}{% if not loop.last %},{% endif %}
{% endfor %}
from {{ ref('users') }}
После компиляции:
select
id,
name,
email
from "jaffle_shop"."main"."users"
Обратите внимание на пробелы и переносы строк: они сохраняются из шаблона. Это особенность Jinja — она не схлопывает whitespace. Об этом будет урок про control flow.
Подробно: comments
Комментарии — только для людей. Они исчезают на этапе компиляции:
{#
Эта модель агрегирует заказы по дням.
Обновляется ежедневно через cron в 06:00 UTC.
#}
select
order_date,
count(*) as orders_count
from {{ ref('stg_jaffle__orders') }}
group by 1
После compile:
select
order_date,
count(*) as orders_count
from "jaffle_shop"."main"."stg_jaffle__orders"
group by 1
Существуют ещё SQL-комментарии (-- и /* ... */). В чём разница?
Правило: если комментарий объясняет Jinja-конструкцию или разработчику — {# #}. Если описывает бизнес-смысл колонки или SQL-логику — --.
Уровни компиляции: что куда смотреть
В dbt есть два каталога для скомпилированных SQL:
target/
├── compiled/ # Скомпилированный SQL после Jinja, без обёртки CREATE TABLE
└── run/ # Полный SQL с CREATE TABLE/VIEW и hooks
target/compiled/jaffle_shop/models/marts/marts__orders.sql:
select
order_date,
count(*) as orders_count
from "jaffle_shop"."main"."stg_jaffle__orders"
group by 1
target/run/jaffle_shop/models/marts/marts__orders.sql:
create table "jaffle_shop"."main"."marts__orders"
as
(
select
order_date,
count(*) as orders_count
from "jaffle_shop"."main"."stg_jaffle__orders"
group by 1
);
Для отладки Jinja — смотрите в target/compiled/. Там результат разворачивания шаблонов без warehouse-обёртки.
Whitespace control: - для подрезки
Шаблоны Jinja сохраняют пробелы и переносы строк. Часто это приводит к корявому output. Чтобы подрезать whitespace, используйте - внутри блока:
{%- set columns = ['id', 'name'] -%}
select
{%- for col in columns %}
{{ col }}{% if not loop.last %},{% endif %}
{%- endfor %}
from {{ ref('users') }}
{%- убирает whitespace перед блоком, -%} — после. Output становится чище:
select
id,
name
from "jaffle_shop"."main"."users"
На junior уровне можете не заморачиваться — лишние пробелы не влияют на корректность SQL. Но в production-проектах команда обычно использует - для читаемости compiled-output.
Hello, Jinja: первый собственный шаблон
Чтобы убедиться, что вы понимаете три конструкции, напишем простую модель:
-- models/playground/hello_jinja.sql
{#
Эта модель печатает текущий target и список колонок.
Учебная — не использовать в production.
#}
{% set my_cols = ['order_id', 'order_date', 'total_amount'] %}
select
'{{ target.name }}' as env,
'{{ target.database }}' as db_name,
{% for col in my_cols -%}
'{{ col }}' as col_{{ loop.index }}{% if not loop.last %},{% endif %}
{% endfor %}
После dbt compile --select hello_jinja:
$ cat target/compiled/jaffle_shop/models/playground/hello_jinja.sql
Вы увидите:
select
'dev' as env,
'jaffle_shop' as db_name,
'order_id' as col_1,
'order_date' as col_2,
'total_amount' as col_3
Это полностью корректный SQL — можете скопировать в DuckDB и запустить. Получится одна строка с пятью колонками.
Распространённые ошибки
1. Перепутать expression и statement.
{# WRONG #}
{{ if x > 0 }}positive{{ endif }}
{# RIGHT #}
{% if x > 0 %}positive{% endif %}
2. Забыть {{ }} вокруг вызова функции.
{# WRONG: вернёт текст 'ref(orders)', не qualifier #}
select * from ref('orders')
{# RIGHT #}
select * from {{ ref('orders') }}
3. Использовать SQL-комментарий внутри Jinja-блока.
{# WRONG: -- внутри {% %} — синтаксическая ошибка Jinja #}
{% set x = 1 -- комментарий %}
{# RIGHT #}
{% set x = 1 %} {# комментарий через Jinja #}
4. Ожидать, что комментарий {# #} сохранится в SQL.
Не сохранится. Если нужен комментарий в финальном SQL — используйте -- или /* */.
Попробуй сам
Напишите модель hello_jinja.sql в каталоге models/playground/. Внутри:
- Комментарий
{# #}с описанием модели. - Statement
{% set %}с переменной — списком из трёх имён колонок. - Запрос
SELECT, который выводит:target.nameкакenv- Каждое имя из списка как отдельная строковая колонка
- SQL-комментарий
-- TODOпослеSELECT.
Выполните dbt compile --select hello_jinja и посмотрите в target/compiled/. Убедитесь, что Jinja-комментарий ушёл, а SQL-комментарий остался.
Итоги
- Jinja2 — шаблонизатор, на котором написан dbt. SQL-файлы — Jinja-шаблоны.
- Три типа блоков:
{{ }}(expression),{% %}(statement),{# #}(comment). - Expression вставляет значение в текст. Statement управляет генерацией. Comment удаляется.
- Скомпилированный SQL — в
target/compiled/, с обёрткойCREATE TABLE— вtarget/run/. - Whitespace в Jinja сохраняется; для подрезки —
-(например,{%- ... -%}). - Не путай Jinja-комментарий с SQL-комментарием: первый исчезает, второй сохраняется.
В следующем уроке — control flow: if, for, set на примерах из реальных dbt-проектов.