Learning Platform
Глоссарий Troubleshooting
Урок 11.03 · 22 мин
Начальный
dbtJinjaexecuterun_queryparse phase

execute flag: parse phase vs execute phase

Это самый «нелогичный» урок Jinja для junior. Многие dbt-разработчики работают год-два, не понимая, почему в их макросе с run_query() иногда возвращается None и ломается компиляция. Корень — в том, что dbt проходит по шаблонам ДВА раза: сначала на этапе парсинга проекта, потом — при запуске. Без понимания этой механики вы будете писать код, который работает «через раз».

Почему два прохода

Чтобы построить DAG, dbt должен сначала прочитать все модели и понять зависимости — кто на кого ссылается через {{ ref() }} и {{ source() }}. Это parse phase. Происходит до того, как любая модель была собрана и любой SQL выполнен.

На parse phase dbt:

  1. Сканирует все .sql, .yml, .csv в проекте.
  2. Парсит Jinja в каждом файле, чтобы выявить ref() и source().
  3. Строит DAG зависимостей.
  4. Валидирует, что все ссылки разрешаются.

Дальше идёт execute phase: dbt в правильном порядке запускает CREATE TABLE ... AS ... для каждой модели, и здесь же второй раз парсит шаблоны — но теперь с полным контекстом (target, warehouse-connection, реальные таблицы).

dbt parseЭтап 1: первый проход по всем шаблонам ради сборки DAG
Извлекаются ref(), source(), config()dbt видит зависимости и регистрирует их в manifest.json
Манифест построенmanifest.json содержит DAG, теперь dbt знает порядок выполнения
dbt run / buildЭтап 2: второй проход, теперь с execute=true
Каждая модель компилируется и выполняетсяТолько сейчас run_query() работает, потому что есть warehouse-connection

Это и есть parse vs execute. И вот в чём подвох: на parse phase warehouse-соединения нет. dbt не подключается к DuckDB/Postgres/Snowflake, пока не пришло время реально выполнять SQL.

Что такое execute flag

Переменная execute — это специальная булева переменная в Jinja-контексте dbt. Она показывает, на какой фазе сейчас исполняется шаблон.

  • На parse phase: execute = false.
  • На execute phase: execute = true.

Проверка: {% if execute %} — «выполни этот блок только на execute phase».

Зачем это нужно? Потому что некоторые функции не работают на parse phase: они требуют warehouse-connection, которого ещё нет.

Главный пример: run_query

run_query(sql) — функция, которая отправляет SQL в warehouse и возвращает результат. Используется, чтобы построить динамический SQL на основе данных в реальной таблице.

Канонический пример: получить список колонок таблицы, чтобы динамически собрать SELECT:

-- models/marts/marts__dynamic_select.sql

{% set cols_query %}
    select column_name 
    from information_schema.columns
    where table_name = 'orders'
{% endset %}

{% set results = run_query(cols_query) %}

{% if execute %}
    {% set column_names = results.columns[0].values() %}
{% else %}
    {% set column_names = [] %}
{% endif %}

select
    {% for col in column_names %}
    {{ col }}{% if not loop.last %},{% endif %}
    {% endfor %}
from {{ ref('stg_jaffle__orders') }}

Что здесь критично:

  1. run_query(cols_query) на parse phase возвращает None, потому что нет warehouse-connection. Если попытаемся вызвать results.columns[0].values() без проверки execute — упадёт с AttributeError: NoneType has no columns.
  2. {% if execute %} пропускает обработку результата на parse phase, ставит заглушку column_names = [].
  3. На execute phase run_query возвращает результат, column_names заполняется реальными колонками.
  4. На parse phase цикл for пройдётся по пустому списку — output будет невалидным SQL, но это не важно: на parse phase SQL и не запускается.

Полный паттерн {% if execute %}

Канонический шаблон работы с run_query:

{% set query %}
    select distinct status from {{ ref('stg_jaffle__orders') }}
{% endset %}

{% set results = run_query(query) %}

{% if execute %}
    {% set statuses = results.columns[0].values() %}
{% else %}
    {% set statuses = [] %}
{% endif %}

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

Это dynamic pivot — pivot, где набор значений не захардкожен, а извлекается из самой таблицы. На parse phase цикл пустой, на execute — генерирует реальный SQL.

WARNING

Запомните паттерн: каждый раз, когда используете run_query для получения данных, оборачивайте обработку результата в {% if execute %} + fallback пустой коллекции. Иначе на parse phase упадёте с None-ошибкой.

Когда execute true, когда false

Команда dbtexecute
dbt parsefalse
dbt listfalse
dbt compiletrue (для всех моделей)
dbt runtrue (на этапе выполнения каждой модели)
dbt testtrue
dbt buildtrue
dbt docs generateсмешано (parse для DAG, execute для catalog)

Ключевая мысль: compile уже execute. dbt compile ходит в warehouse за метаданными для адаптера и выполняет run_query-вызовы. Поэтому в скомпилированный SQL уже подставляются результаты.

dbt parse — особая команда, чисто для построения manifest без обращения к warehouse. Полезна в CI для проверки синтаксиса.

Тонкость: что run_query возвращает

run_query возвращает объект AgateTable (agate — Python-библиотека табличных данных). У него есть:

  • .columns — список объектов колонок. results.columns[0] — первая колонка.
  • .column_names — список имён колонок.
  • .rows — список строк.
  • len(results) — количество строк (но только на execute!).

Чтобы получить значения одной колонки как Python-список:

{% set values = results.columns[0].values() %}

Или через rows и индекс:

{% set values = [] %}
{% for row in results.rows %}
    {% do values.append(row[0]) %}
{% endfor %}

Оба работают идентично.

Альтернатива run_query: dbt_utils.get_column_values

В пакете dbt_utils есть готовая макрос для частого случая «получить уникальные значения колонки»:

{% set statuses = dbt_utils.get_column_values(
    table=ref('stg_jaffle__orders'),
    column='status'
) %}

Внутри она делает run_query и обрабатывает execute правильно. Возвращает пустой список на parse phase, реальный — на execute. Если ваш случай — просто получить уникальные значения, используйте get_column_values, а не свой run_query. Меньше boilerplate, меньше шансов забыть {% if execute %}.

Когда execute НЕ нужен

Не все макросы требуют execute. Главное правило: execute нужен только там, где есть run_query или другой warehouse-доступ.

Простые operation типа арифметики, конкатенации строк, работы с переменными — не зависят от execute:

{# Не нужен execute — нет warehouse-обращения #}
{% set lookback = var('lookback_days', 7) %}
{% set table_name = ref('orders') %}
{% set columns_str = ['a', 'b', 'c'] | join(', ') %}

Эти строки работают одинаково на parse и execute.

Распространённая ошибка #1: использовать results без execute

{# WRONG #}
{% set results = run_query('select count(*) from orders') %}
{% set count = results.columns[0].values()[0] %}
select '{{ count }}' as cnt

{# Падает на parse: NoneType has no attribute 'columns' #}

Исправление:

{# RIGHT #}
{% set results = run_query('select count(*) from orders') %}
{% if execute %}
    {% set count = results.columns[0].values()[0] %}
{% else %}
    {% set count = 0 %}
{% endif %}
select '{{ count }}' as cnt

Распространённая ошибка #2: тянуть данные на parse

Иногда нужно понять, что в источнике X записей, и от этого что-то менять. Соблазн:

{# WRONG: пытаемся выполнить SELECT на parse, когда warehouse недоступен #}
{% set count = run_query('select count(*) from raw_orders').columns[0].values()[0] %}
{% if count > 1000000 %}
    -- большой объём
{% else %}
    -- маленький
{% endif %}

Это работает на dbt compile и dbt run, но на dbt parse и dbt list падает. Хуже: некоторые CI делают dbt parse перед dbt run — и тут вы получаете сюрприз.

Исправление — оборачивать всё, что требует warehouse, в {% if execute %}:

{% set count = 0 %}
{% if execute %}
    {% set results = run_query('select count(*) from raw_orders') %}
    {% set count = results.columns[0].values()[0] %}
{% endif %}

{% if count > 1000000 %}
    -- ...
{% endif %}

Распространённая ошибка #3: ожидать, что переменная сохранится между моделями

Нет, не сохранится. Каждая модель — отдельный Jinja-контекст. {% set x = 5 %} в model A не виден в model B.

Если нужна общая переменная — используйте var() (из dbt_project.yml), env_var(), или macro, который возвращает значение.

Tradeoff: насколько dynamic делать модели

run_query — мощная, но опасная вещь:

  • Плюс: SQL не хардкодит список значений, автоматически адаптируется.
  • Минус: компиляция модели зависит от данных — если завтра в warehouse появится новый status, SQL изменится, нужен dbt run. В CI это может приводить к нестабильным diff.
  • Минус: лишнее обращение к warehouse при каждой компиляции. На больших проектах удлиняет dbt compile.

Junior-правило: используйте run_query только когда реально нужен динамизм. Захардкоженный список (['completed', 'pending', 'returned']) обычно нормально — его легко обновить.

Попробуй сам

Напишите модель marts__status_pivot.sql, которая:

  1. Через dbt_utils.get_column_values получает список уникальных значений status из stg_jaffle__orders.
  2. Делает pivot: каждая строка = customer_id, колонки = count в каждом status.

Скелет:

{% 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 compile модель правильно собирается. Затем выполните dbt parse — не должно быть ошибок (потому что get_column_values корректно обрабатывает execute=false внутри).

Проверка знанийKnowledge check
Что произойдёт с моделью, в которой run_query вызван БЕЗ обёртки `{% if execute %}`, при команде dbt parse? А при dbt compile?
ОтветAnswer
При dbt parse: execute=false, нет warehouse-connection. run_query возвращает None. Если в коде есть results.columns или results.rows — упадёт с AttributeError 'NoneType has no attribute columns'. dbt parse завершится ошибкой, manifest не построится. Это блокирующий случай — CI красный, никто не может работать с проектом. При dbt compile: execute=true, warehouse доступен. run_query реально выполняется, возвращает AgateTable. Дальнейший код работает, модель компилируется без ошибок. Главный риск: разработчик пишет код без if execute, тестирует через dbt run/compile — всё работает. Потом этот код попадает в CI, где есть шаг dbt parse — падает. Часто это вскрывается в самый неподходящий момент. Правило: каждый run_query всегда оборачиваешь в if execute с fallback пустой коллекции. Это страховка от parse-phase падения и must-know паттерн для любого, кто пишет custom macros или dynamic models.
Проверка знанийKnowledge check
В каких сценариях имеет смысл использовать run_query в модели, а в каких лучше захардкодить список значений?
ОтветAnswer
run_query разумен, когда: - Список значений ИЗМЕНЯЕТСЯ часто и список нужен 'из коробки' — например, статусы заказов, типы платежей. Когда добавляется новое значение, не нужно править модель. - Динамические pivot или unpivot, где число колонок не известно заранее. - Меta-программирование: получить все колонки таблицы, исключить пароли, сделать SELECT с остальными. Хардкод значений лучше, когда: - Список значений известен и редко меняется (например, страны мира, валюты, dim_calendar). - Cтабильность compile важнее — захардкоженный список = тот же скомпилированный SQL независимо от данных warehouse. - В CI важна детерминированность: с run_query параллельный CI-job может получить разный список и разный SQL. - Когда run_query вызывается из macro и заметно замедляет dbt compile (для каждой модели — отдельный SELECT в warehouse). Junior-эвристика: начинай с захардкоженного списка. Переходи на run_query только когда чувствуешь боль от ручной поддержки списка. Часто проще написать сложный список один раз, чем дебажить плавающий dynamic SQL в production.

Итоги

  • dbt делает два прохода по шаблонам: parse phase (без warehouse) и execute phase (с warehouse).
  • execute — boolean, false на parse, true на execute.
  • run_query возвращает None на parse phase. Всегда обрабатывай результат под {% if execute %} с fallback.
  • dbt parse, dbt list — только parse. dbt compile, dbt run, dbt test, dbt build — parse + execute.
  • dbt_utils.get_column_values — готовая абстракция над частым случаем run_query. Используй её вместо ручного run_query, когда возможно.
  • Tradeoff: dynamic SQL даёт гибкость ценой стабильности. Хардкод проще и предсказуемее.
run_query и statement: introspection

В следующем уроке — последний кусок Jinja-фундамента: log() и приёмы отладки через target/compiled и target/run.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 7. В чём разница между parse phase и execute phase в dbt?

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

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

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

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