Перейти к основному содержимому

Использование Jinja для улучшения вашего SQL-кода

Обновлен
Jinja
dbt Core
Advanced
Menu

    Введение

    В этом руководстве мы рассмотрим распространенный шаблон, используемый в SQL, и затем применим Jinja для улучшения нашего кода.

    Если вы хотите проработать этот запрос, добавьте этот CSV в папку seeds/ вашего проекта dbt, а затем выполните dbt seed.

    Работая над шагами этой модели, мы рекомендуем держать открытым ваш скомпилированный SQL, чтобы проверить, во что компилируется ваш Jinja. Для этого:

    • Используя dbt Cloud: Нажмите кнопку компиляции, чтобы увидеть скомпилированный SQL в правой панели
    • Используя dbt Core: Запустите dbt compile из командной строки. Затем откройте скомпилированный SQL-файл в директории target/compiled/{project name}/. Используйте разделение экрана в вашем редакторе кода, чтобы держать оба файла открытыми одновременно.

    Написание SQL без Jinja

    Рассмотрим модель данных, в которой order может иметь много payments. Каждый payment может иметь payment_method из bank_transfer, credit_card или gift_card, и, следовательно, каждый order может иметь несколько payment_methods.

    С аналитической точки зрения важно знать, какая часть каждого order была оплачена с помощью каждого payment_method. В вашем проекте dbt вы можете создать модель с именем order_payment_method_amounts с следующим SQL:

    models/order_payment_method_amounts.sql
    select
    order_id,
    sum(case when payment_method = 'bank_transfer' then amount end) as bank_transfer_amount,
    sum(case when payment_method = 'credit_card' then amount end) as credit_card_amount,
    sum(case when payment_method = 'gift_card' then amount end) as gift_card_amount,
    sum(amount) as total_amount
    from {{ ref('raw_payments') }}
    group by 1

    SQL для каждой суммы по методу оплаты повторяется, что может быть сложно поддерживать по ряду причин:

    • Если логика или имя поля изменится, код нужно будет обновить в трех местах.
    • Часто этот код создается путем копирования и вставки, что может привести к ошибкам.
    • Другие аналитики, которые просматривают код, с меньшей вероятностью заметят ошибки, так как обычно только просматривают повторяющийся код.

    Поэтому мы собираемся использовать Jinja, чтобы помочь нам очистить его, или сделать наш код более "DRY" ("Don't Repeat Yourself" - "Не повторяйся").

    Использование цикла for в моделях для повторяющегося SQL

    Здесь повторяющийся код можно заменить циклом for. Следующий код будет скомпилирован в тот же запрос, но его значительно легче поддерживать.

    /models/order_payment_method_amounts.sql
    select
    order_id,
    {% for payment_method in ["bank_transfer", "credit_card", "gift_card"] %}
    sum(case when payment_method = '{{payment_method}}' then amount end) as {{payment_method}}_amount,
    {% endfor %}
    sum(amount) as total_amount
    from {{ ref('raw_payments') }}
    group by 1

    Установка переменных в начале модели

    Мы рекомендуем устанавливать переменные в начале модели, так как это помогает с читаемостью и позволяет ссылаться на список в нескольких местах, если это необходимо. Это практика, которую мы заимствовали из многих других языков программирования.

    /models/order_payment_method_amounts.sql
    {% set payment_methods = ["bank_transfer", "credit_card", "gift_card"] %}

    select
    order_id,
    {% for payment_method in payment_methods %}
    sum(case when payment_method = '{{payment_method}}' then amount end) as {{payment_method}}_amount,
    {% endfor %}
    sum(amount) as total_amount
    from {{ ref('raw_payments') }}
    group by 1

    Использование loop.last для избежания завершающих запятых

    В приведенном выше запросе наш последний столбец находится вне цикла for. Однако это может быть не всегда так. Если последняя итерация цикла является нашим последним столбцом, нам нужно убедиться, что в конце нет завершающей запятой.

    Мы часто используем оператор if вместе с переменной Jinja loop.last, чтобы убедиться, что не добавляем лишнюю запятую:

    /models/order_payment_method_amounts.sql
    {% set payment_methods = ["bank_transfer", "credit_card", "gift_card"] %}

    select
    order_id,
    {% for payment_method in payment_methods %}
    sum(case when payment_method = '{{payment_method}}' then amount end) as {{payment_method}}_amount
    {% if not loop.last %},{% endif %}
    {% endfor %}
    from {{ ref('raw_payments') }}
    group by 1

    Альтернативный способ записи этого: {{ "," if not loop.last }}.

    Использование управления пробелами для упорядочивания скомпилированного кода

    Если вы проверяли свой код в папке target/compiled, вы могли заметить, что этот код приводит к большому количеству пробелов:

    target/compiled/jaffle_shop/order_payment_method_amounts.sql


    select
    order_id,

    sum(case when payment_method = 'bank_transfer' then amount end) as bank_transfer_amount
    ,

    sum(case when payment_method = 'credit_card' then amount end) as credit_card_amount
    ,

    sum(case when payment_method = 'gift_card' then amount end) as gift_card_amount


    from raw_jaffle_shop.payments
    group by 1

    Мы можем использовать управление пробелами, чтобы упорядочить наш код:

    models/order_payment_method_amounts.sql
    {%- set payment_methods = ["bank_transfer", "credit_card", "gift_card"] -%}

    select
    order_id,
    {%- for payment_method in payment_methods %}
    sum(case when payment_method = '{{payment_method}}' then amount end) as {{payment_method}}_amount
    {%- if not loop.last %},{% endif -%}
    {% endfor %}
    from {{ ref('raw_payments') }}
    group by 1

    Правильное управление пробелами часто требует много проб и ошибок! Мы рекомендуем отдавать приоритет читаемости вашего кода модели над читаемостью скомпилированного кода и делать это только как дополнительную полировку.

    Использование макроса для возврата методов оплаты

    Здесь мы жестко закодировали список методов оплаты в нашей модели. Возможно, нам потребуется доступ к этому списку из другой модели. Хорошим решением здесь будет использование переменной, но для целей этого руководства мы вместо этого будем использовать макрос!

    Макросы в Jinja — это фрагменты кода, которые можно вызывать несколько раз — они аналогичны функции в Python и чрезвычайно полезны, если вы обнаружите, что повторяете код в нескольких моделях.

    Наш макрос просто вернет список методов оплаты:

    /macros/get_payment_methods.sql
    {% macro get_payment_methods() %}
    {{ return(["bank_transfer", "credit_card", "gift_card"]) }}
    {% endmacro %}

    Здесь стоит отметить несколько моментов:

    • Обычно макросы принимают аргументы — мы увидим это позже, но пока нам все равно нужно настроить наш макрос с пустыми скобками, где обычно находятся аргументы (т.е. get_payment_methods())
    • Мы использовали функцию return, чтобы вернуть список — без этой функции макрос вернул бы строку.

    Теперь, когда у нас есть макрос для наших методов оплаты, мы можем обновить нашу модель следующим образом:

    models/order_payment_method_amounts.sql
    {%- set payment_methods = get_payment_methods() -%}

    select
    order_id,
    {%- for payment_method in payment_methods %}
    sum(case when payment_method = '{{payment_method}}' then amount end) as {{payment_method}}_amount
    {%- if not loop.last %},{% endif -%}
    {% endfor %}
    from {{ ref('raw_payments') }}
    group by 1

    Обратите внимание, что мы не использовали фигурные скобки при вызове макроса — мы уже находимся внутри оператора Jinja, поэтому нет необходимости снова использовать скобки.

    Динамическое получение списка методов оплаты

    До сих пор мы жестко кодировали список возможных методов оплаты. Если будет введен новый payment_method или один из существующих методов будет переименован, список нужно будет обновить.

    Однако в любой момент времени вы можете узнать, какие payment_methods используются для оплаты, выполнив следующий запрос:

    select distinct
    payment_method
    from {{ ref('raw_payments') }}
    order by 1

    Операторы предоставляют способ выполнения этого запроса и возврата результатов в ваш контекст Jinja. Это означает, что список payment_methods может быть установлен на основе данных в вашей базе данных, а не жестко заданного значения.

    Самый простой способ использовать оператор — через макрос run_query. Для первой версии давайте проверим, что мы получаем из базы данных, записав результаты в командную строку с помощью функции log.

    macros/get_payment_methods.sql
    {% macro get_payment_methods() %}

    {% set payment_methods_query %}
    select distinct
    payment_method
    from {{ ref('raw_payments') }}
    order by 1
    {% endset %}

    {% set results = run_query(payment_methods_query) %}

    {{ log(results, info=True) }}

    {{ return([]) }}

    {% endmacro %}

    Командная строка возвращает нам следующее:

    | column         | data_type |
    | -------------- | --------- |
    | payment_method | Text |

    Это на самом деле таблица Agate. Чтобы получить методы оплаты в виде списка, нам нужно выполнить некоторые дополнительные преобразования.

    {% macro get_payment_methods() %}

    {% set payment_methods_query %}
    select distinct
    payment_method
    from {{ ref('raw_payments') }}
    order by 1
    {% endset %}

    {% set results = run_query(payment_methods_query) %}

    {% if execute %}
    {# Возвращаем первый столбец #}
    {% set results_list = results.columns[0].values() %}
    {% else %}
    {% set results_list = [] %}
    {% endif %}

    {{ return(results_list) }}

    {% endmacro %}

    Здесь есть несколько сложных моментов:

    • Мы использовали переменную execute, чтобы убедиться, что код выполняется на этапе parse dbt (иначе будет выдана ошибка).
    • Мы использовали методы Agate, чтобы вернуть столбец в виде списка.

    К счастью, наш код модели не нужно обновлять, так как мы уже вызываем макрос для получения списка методов оплаты. И теперь любые новые payment_methods, добавленные в базовую модель данных, будут автоматически обрабатываться моделью dbt.

    Написание модульных макросов

    Возможно, вы захотите использовать аналогичный шаблон в другом месте вашего проекта dbt. В результате вы решаете разбить вашу логику на два отдельных макроса — один для общего возврата столбца из отношения, и другой, который вызывает этот макрос с правильными аргументами, чтобы получить список методов оплаты.

    macros/get_payment_methods.sql
    {% macro get_column_values(column_name, relation) %}

    {% set relation_query %}
    select distinct
    {{ column_name }}
    from {{ relation }}
    order by 1
    {% endset %}

    {% set results = run_query(relation_query) %}

    {% if execute %}
    {# Возвращаем первый столбец #}
    {% set results_list = results.columns[0].values() %}
    {% else %}
    {% set results_list = [] %}
    {% endif %}

    {{ return(results_list) }}

    {% endmacro %}


    {% macro get_payment_methods() %}

    {{ return(get_column_values('payment_method', ref('raw_payments'))) }}

    {% endmacro %}

    Использование макроса из пакета

    Макросы позволяют аналитикам применять принципы программной инженерии к SQL, который они пишут. Одна из особенностей макросов, которая делает их еще более мощными, — это их способность быть общими для разных проектов.

    Ряд полезных макросов dbt уже написаны в пакете dbt-utils. Например, макрос get_column_values из dbt-utils можно использовать вместо макроса get_column_values, который мы написали сами (сэкономив нам много времени, но по крайней мере мы чему-то научились по пути!).

    Установите dbt-utils пакет в вашем проекте (документация здесь), а затем обновите вашу модель, чтобы использовать макрос из пакета:

    models/order_payment_method_amounts.sql
    {%- set payment_methods = dbt_utils.get_column_values(
    table=ref('raw_payments'),
    column='payment_method'
    ) -%}

    select
    order_id,
    {%- for payment_method in payment_methods %}
    sum(case when payment_method = '{{payment_method}}' then amount end) as {{payment_method}}_amount
    {%- if not loop.last %},{% endif -%}
    {% endfor %}
    from {{ ref('raw_payments') }}
    group by 1

    Затем вы можете удалить макросы, которые мы создали на предыдущих шагах. Всякий раз, когда вы пытаетесь решить проблему, которую, как вы думаете, другие могли решить ранее, стоит проверить пакет dbt-utils, чтобы увидеть, не поделился ли кто-то своим кодом!

    0