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

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

Что произошло:

  1. Комментарий {# ... #} удалён.
  2. {% set table_name = 'orders' %} выполнен, переменная создана, в output ничего не вывелось (statement не выводит).
  3. {{ ref(table_name) }} — expression: вычислено как строка "jaffle_shop"."main"."orders", подставлено вместо {{ }}.

Когда использовать каждое

Нужно вставить значение в текстref, source, var, переменная, результат функции
Используй expression-блокExpression: вычисляется, подставляется в SQL
Нужно управлять логикой генерации SQLif, for, set, macro, include
Используй statement-блокStatement: выполняется, в output ничего не идёт
Нужен комментарий ТОЛЬКО для разработчикаОбъяснение, TODO — не должно попасть в SQL
Используй {# #}Comment: вырезается на этапе compile

Главное правило, которое путает 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
NOTE

В курсе вы часто увидите '{{ 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/. Внутри:

  1. Комментарий {# #} с описанием модели.
  2. Statement {% set %} с переменной — списком из трёх имён колонок.
  3. Запрос SELECT, который выводит:
    • target.name как env
    • Каждое имя из списка как отдельная строковая колонка
  4. SQL-комментарий -- TODO после SELECT.

Выполните dbt compile --select hello_jinja и посмотрите в target/compiled/. Убедитесь, что Jinja-комментарий ушёл, а SQL-комментарий остался.

Проверка знанийKnowledge check
В чём разница между Jinja-комментарием и SQL-комментарием в dbt-модели? Какой использовать когда?
ОтветAnswer
Jinja-комментарий обёрнут в фигурные скобки и решётку (открывающий блок начинается с { %23 и заканчивается %23 }). Удаляется на этапе compile, в финальный SQL и target/compiled/ не попадает. SQL-комментарий это -- или /* ... */. Сохраняется в compile, попадает в target/compiled/, в target/run/ и в warehouse query history. Когда использовать каждый: - Jinja-комментарий: объяснение Jinja-логики, ремарки для разработчика, TODO про шаблон, заметки 'эта переменная нужна для dispatch'. Не должно попадать в production SQL. - SQL-комментарий: бизнес-описание колонки, ссылка на бизнес-правило, заметка для DBA, которая может попасть в lineage warehouse'а. Например: '-- Поле updated_at используется для incremental, не удалять'. Правило большого пальца: если коммент имеет смысл только для того, кто читает .sql-файл — Jinja. Если коммент должен сопровождать SQL дальше (например, в EXPLAIN, в логах warehouse) — SQL-комментарий.
Проверка знанийKnowledge check
Что выведет в скомпилированный SQL такой шаблон, и почему: `{% set name = 'orders' %}` select * from `{{ ref(name) }}` {# comment #}
ОтветAnswer
Скомпилированный SQL будет содержать (для DuckDB-target): select * from "jaffle_shop"."main"."orders" Что произошло: 1. ''{% set name = 'orders' %}'' — это statement, он создал переменную name = 'orders', но в output ничего не вывел. Останется пустая строка/пробел (whitespace). 2. ''{{ ref(name) }}'' — expression. Jinja вычисляет ref('orders'), получает Relation объект, который при подстановке в текст превращается в полный qualifier "jaffle_shop"."main"."orders". Подставляется вместо ''{{ }}'' в SQL. 3. {# comment #} — Jinja-комментарий. Удаляется компилятором, в output не попадает. Останется пустая позиция (пробелы). Финальный SQL валиден и выполнится в DuckDB. Это базовый паттерн: parameterize имя модели через переменную, использовать ''{{ ref() }}'' для подстановки.

Итоги

  • Jinja2 — шаблонизатор, на котором написан dbt. SQL-файлы — Jinja-шаблоны.
  • Три типа блоков: {{ }} (expression), {% %} (statement), {# #} (comment).
  • Expression вставляет значение в текст. Statement управляет генерацией. Comment удаляется.
  • Скомпилированный SQL — в target/compiled/, с обёрткой CREATE TABLE — в target/run/.
  • Whitespace в Jinja сохраняется; для подрезки — - (например, {%- ... -%}).
  • Не путай Jinja-комментарий с SQL-комментарием: первый исчезает, второй сохраняется.
Jinja в Airflow

В следующем уроке — control flow: if, for, set на примерах из реальных dbt-проектов.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 7. Какая Jinja-конструкция вставляет в SQL результат вычисления выражения?

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

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

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

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