1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【dbt入門】dbtを理解するために知っておきたいJinja2の知識

1
Posted at

はじめに

dbtのSQLファイルを開くと、見慣れない {{ }}{% %} という記法が登場します。これらはすべて Jinja2 というテンプレートエンジンの構文です。

dbtはSQLの中にJinja2を組み込むことで、変数、条件分岐、ループ、関数(マクロ)といったプログラミング的な機能をSQLに持たせています。Jinja2を理解することは、dbtを使いこなすうえで避けて通れないステップです。

この記事では、dbt初学者がまず押さえておくべきJinja2の基礎知識を体系的に整理します。

この記事ではJinja2の全機能を網羅するのではなく、dbtを使ううえで実際に必要になる知識 に絞って解説します。

Jinja2とは

Jinja2は、Python製の テンプレートエンジン です。テンプレートエンジンとは、テンプレート(雛形)の中に動的な値や制御構文を埋め込み、最終的なテキストを生成する仕組みのことです。

もともとはWebアプリケーション(Flask など)でHTMLを動的に生成するために広く使われていますが、dbtではこの仕組みを SQLの動的生成 に応用しています。

dbtがJinja2を採用している理由

素のSQLだけでは、以下のようなことが難しくなります。

  • 環境(dev / prod)に応じてスキーマ名を切り替える
  • 複数カラムに同じ変換処理を繰り返す
  • 共通のSQL処理を再利用する

Jinja2を組み合わせることで、SQLの表現力が大幅に広がり、DRY(Don't Repeat Yourself)な開発が可能になります。

-- Jinja2なし:スキーマ名がハードコードされている
select * from my_project.prod_schema.orders

-- Jinja2あり:環境に応じて自動で切り替わる
select * from {{ ref('stg_orders') }}

Jinja2の3つの基本構文

Jinja2の構文は大きく3種類です。dbtのSQLファイル内で見かける {{ }}{% %}{# #} はすべてこの3つのどれかに該当します。

{{ }} — 式の出力(Expression)

値を評価して、その結果をSQLに埋め込みます。dbtで最も多く目にする構文です。

-- ref() の戻り値がSQL内に展開される
select * from {{ ref('stg_orders') }}

-- 変数の値を埋め込む
where status = '{{ var("target_status") }}'

コンパイル後は、Jinja部分が実際の値に置き換わります。

-- コンパイル後
select * from my_project.dev_schema.stg_orders
where status = 'active'

{% %} — 制御構文(Statement)

条件分岐やループなどの制御ロジックを記述します。この構文自体はSQLに出力されず、SQLの生成過程を制御します。

{% if target.name == 'prod' %}
    select * from prod_schema.orders
{% else %}
    select * from dev_schema.orders limit 100
{% endif %}

{# #} — コメント

Jinja2のコメントです。コンパイル後のSQLには一切出力されません。SQLのコメント(--)とは異なり、最終的なSQLに痕跡を残しません。

{# このコメントはコンパイル後のSQLには出力されない #}
select * from {{ ref('stg_orders') }}

-- このSQLコメントはコンパイル後も残る

3つの構文をまとめると以下の通りです。

構文 名前 役割 SQLに出力されるか
{{ }} Expression 値の埋め込み される(評価結果が出力)
{% %} Statement 制御ロジック されない
{# #} Comment コメント されない

dbtで頻出するJinja構文

Jinja2の基本構文を使って、dbtは独自の関数を提供しています。ここでは特に使用頻度の高い4つを紹介します。

{{ ref() }} — モデル間の参照

dbtで最も重要な関数です。他のモデルを参照する際に使います。ref() を使うことで、dbtがモデル間の依存関係を自動で認識し、正しい実行順序を決定します。

select * from {{ ref('stg_orders') }}
-- コンパイル後(dev環境の場合)
select * from my_project.dev_schema.stg_orders

ref() を使わずにテーブル名を直接書くと、dbtは依存関係を検知できません。DAGが正しく構築されず、実行順序のエラーが起きる原因になります。モデル間の参照には必ず ref() を使いましょう。

{{ source() }} — ソーステーブルの参照

dbtの管理外にあるソーステーブル(Bronze層)を参照する関数です。sources.yml で定義したソース名とテーブル名を引数に取ります。

select * from {{ source('raw', 'orders') }}
-- コンパイル後
select * from my_project.bronze.orders

{{ config() }} — モデル設定の定義

モデルのマテリアライズ方式やスキーマなどの設定をSQLファイルの先頭で指定します。

{{ config(
    materialized='incremental',
    unique_key='order_id',
    schema='GOLD_DBT'
) }}

select * from {{ ref('stg_orders') }}

config() はSQLに出力されません。dbtがモデルの設定情報として内部的に処理します。

{{ var() }} — 変数の参照

dbt_project.yml やコマンドラインで定義した変数を参照します。環境ごとに値を切り替えたい場合に便利です。

dbt_project.yml
vars:
  start_date: '2024-01-01'
select *
from {{ ref('stg_orders') }}
where order_date >= '{{ var("start_date") }}'

コマンドラインから変数を上書きすることもできます。

dbt run --vars '{"start_date": "2025-01-01"}'

制御構文を使いこなす

{% %} を使った制御構文により、SQLを動的に生成できます。ここでは代表的な3つの構文を紹介します。

{% if %} — 条件分岐

条件に応じて出力するSQLを切り替えます。

select
    order_id,
    amount,
    {% if target.name == 'prod' %}
        customer_email
    {% else %}
        md5(customer_email) as customer_email  {# dev環境ではマスク #}
    {% endif %}
from {{ ref('stg_orders') }}

target.name はdbtの実行ターゲット(dev / prod など)を返す組み込み変数です。本番環境と開発環境で異なるSQLを生成したい場合によく使われます。

dbtにはインクリメンタルモデル用の組み込み関数 is_incremental() もあります。

{{ config(materialized='incremental') }}

select * from {{ ref('stg_orders') }}

{% if is_incremental() %}
    where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

{% for %} — ループ処理

リストの要素に対して繰り返し処理を行います。複数カラムに同じ変換を適用したい場合に特に有効です。

{% set payment_methods = ['credit_card', 'bank_transfer', 'gift_card'] %}

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

コンパイル後は、ループが展開されたSQLが生成されます。

-- コンパイル後
select
    order_id,
    sum(case when payment_method = 'credit_card' then amount else 0 end)
        as credit_card_amount,
    sum(case when payment_method = 'bank_transfer' then amount else 0 end)
        as bank_transfer_amount,
    sum(case when payment_method = 'gift_card' then amount else 0 end)
        as gift_card_amount
from my_project.dev_schema.stg_payments
group by order_id

loop.last はJinja2の組み込み変数で、ループの最後の要素かどうかを判定します。末尾のカンマを制御するために頻繁に使われるパターンです。

{% set %} — 変数の定義

Jinjaテンプレート内でローカル変数を定義します。繰り返し使う値やリストをまとめておくのに便利です。

{% set target_status = 'active' %}
{% set columns = ['order_id', 'customer_id', 'amount', 'status'] %}

select
    {% for col in columns %}
        {{ col }}{% if not loop.last %},{% endif %}
    {% endfor %}
from {{ ref('stg_orders') }}
where status = '{{ target_status }}'

SQLクエリの結果を変数に格納する run_query() と組み合わせることも可能です。

{% set results = run_query("select distinct status from " ~ ref('stg_orders')) %}
{% set statuses = results.columns[0].values() %}

{# statuses を使ってSQLを動的に生成 #}

マクロ(macro)— SQLの関数化

マクロとは何か

マクロは、再利用可能なJinjaのコードブロックです。プログラミング言語でいう「関数」に相当します。共通のSQL処理をマクロとして定義しておけば、複数のモデルから呼び出して使えます。

マクロの定義方法と呼び出し方

マクロは macros/ ディレクトリに .sql ファイルとして配置します。

macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name, precision=2) %}
    round(cast({{ column_name }} as numeric) / 100, {{ precision }})
{% endmacro %}

モデルからは {{ }} で呼び出します。

models/marts/fct_orders.sql
select
    order_id,
    {{ cents_to_dollars('amount_cents') }} as amount_dollars,
    {{ cents_to_dollars('tax_cents', 4) }} as tax_dollars
from {{ ref('stg_orders') }}

コンパイル後は、マクロの中身が展開されたSQLが生成されます。

-- コンパイル後
select
    order_id,
    round(cast(amount_cents as numeric) / 100, 2) as amount_dollars,
    round(cast(tax_cents as numeric) / 100, 4) as tax_dollars
from my_project.dev_schema.stg_orders

実践例:よく使うSQL処理をマクロ化する

日付の切り捨て処理は、データウェアハウスごとに関数名が異なります。マクロにしておけば、DWHを切り替えても修正箇所が1か所で済みます。

macros/date_trunc.sql
{% macro date_trunc(datepart, column) %}
    {% if target.type == 'snowflake' %}
        date_trunc('{{ datepart }}', {{ column }})
    {% elif target.type == 'bigquery' %}
        date_trunc({{ column }}, {{ datepart }})
    {% endif %}
{% endmacro %}
models/marts/fct_daily_revenue.sql
select
    {{ date_trunc('month', 'order_date') }} as order_month,
    sum(amount) as total_revenue
from {{ ref('stg_orders') }}
group by 1

dbtのパッケージ(例:dbt-utils)には、こうした汎用マクロが多数用意されています。自分で書く前に既存のパッケージを確認するのがおすすめです。

フィルター — 値の加工

Jinja2のフィルター構文(|

フィルターは、パイプ(|)を使って値を加工する仕組みです。Pythonのメソッドチェーンのような感覚で使えます。

{# 基本構文 #}
{{  | フィルター名 }}

{# フィルターの連結もできる #}
{{  | フィルター1 | フィルター2 }}

dbtでよく使うフィルター

フィルター 動作 結果
upper 大文字に変換 {{ "hello" | upper }} HELLO
lower 小文字に変換 {{ "HELLO" | lower }} hello
trim 前後の空白を除去 {{ " hello " | trim }} hello
default 値がない場合のデフォルト値 {{ undefined_var | default("N/A") }} N/A
join リストを結合 {{ ["a", "b", "c"] | join(", ") }} a, b, c
length 要素数を返す {{ [1, 2, 3] | length }} 3
replace 文字列の置換 {{ "foo_bar" | replace("_", "-") }} foo-bar

default フィルターは var() と組み合わせて、変数が未定義の場合のフォールバック値を設定するパターンでよく使われます。

{% set start = var('start_date', none) %}

select *
from {{ ref('stg_orders') }}
{% if start %}
    where order_date >= '{{ start }}'
{% endif %}

空白制御(Whitespace Control)

{%- -%} / {{- -}} の意味

Jinja2の制御構文やコメントは、コンパイル時に空行や余分な空白を残すことがあります。ハイフン(-)をタグの内側に付けることで、前後の空白を除去できます。

{# ハイフンなし:空白が残る #}
{% for col in columns %}
    {{ col }},
{% endfor %}

{# ハイフンあり:空白を除去 #}
{%- for col in columns -%}
    {{ col }},
{%- endfor -%}
記法 動作
{%- ... %} タグの の空白を除去
{% ... -%} タグの の空白を除去
{%- ... -%} タグの 前後 の空白を除去

生成されるSQLが崩れるときの対処法

コンパイル後のSQLに意図しない空行やインデントの乱れがある場合は、以下の手順で対処します。

  1. dbt compile を実行し、target/compiled/ のSQLを確認する
  2. 空白が気になる箇所の {% %}- を付けてみる
  3. 再度 dbt compile して結果を確認する

多少の空白や空行があってもSQLの実行結果には影響しません。可読性が著しく損なわれない限り、空白制御に神経質になりすぎる必要はありません。

デバッグのコツ

Jinja2を使ったSQLは、コンパイル前と実行されるSQLが異なるため、デバッグの方法を知っておくことが重要です。

dbt compile でコンパイル結果を確認する

dbt compile はJinjaテンプレートを実際のSQLに変換しますが、データウェアハウスには実行しません。「意図通りのSQLが生成されているか」を確認するのに最も基本的な手段です。

dbt compile --select my_model

コンパイル結果は target/compiled/ に出力されます。

target/compiled/my_project/models/marts/fct_orders.sql

{{ log() }} でデバッグ出力する

log() 関数を使うと、コンパイル時にターミナルにメッセージを出力できます。変数の中身を確認したいときに便利です。

{% set my_var = var('start_date', '2024-01-01') %}
{{ log("start_date の値: " ~ my_var, info=true) }}

select *
from {{ ref('stg_orders') }}
where order_date >= '{{ my_var }}'

ターミナルに以下のように出力されます。

start_date の値: 2024-01-01

log() の第2引数に info=true を指定しないと出力されません。デバッグ時は必ず付けましょう。

target/compiled/ を覗く習慣をつける

Jinjaを使ったモデルで問題が起きたとき、まず確認すべきは target/compiled/ に出力されたSQLです。

  • SQLの構文エラー → コンパイル結果のSQLに構文ミスがないか確認
  • データが想定と違うref()source() が正しいテーブルを指しているか確認
  • 条件分岐が効いていない{% if %} ブロックの展開結果を確認

この「コンパイル後のSQLを見る」という習慣が、dbt開発のデバッグ効率を大きく向上させます。

Jinja2を学ぶうえでの注意点

dbt独自の関数はJinja2の標準機能ではない

ref()source()config()var() などは dbtが独自に提供している関数 であり、Jinja2の標準機能ではありません。Jinja2の公式ドキュメントを読んでもこれらの関数は出てきません。

提供元 関数・構文の例
Jinja2標準 {% if %}, {% for %}, {% set %}, {% macro %}, フィルター
dbt独自 ref(), source(), config(), var(), log(), run_query(), is_incremental(), this

Jinja2の基本構文({{ }}{% %}、フィルターなど)はJinja2の公式ドキュメントで、dbt独自の関数はdbtの公式ドキュメント(Jinja and macros)で学ぶ、と覚えておくと情報源を迷いません。

SQLの中にJinjaを書く感覚に慣れるために

dbt特有の「SQLの中にJinjaを混ぜる」スタイルは、最初は違和感があるかもしれません。慣れるためのアドバイスをいくつか紹介します。

まずは ref()source() だけ覚える。 この2つだけで多くのモデルは書けます。{% if %}{% for %} は必要になったタイミングで学べば十分です。

常に dbt compile で確認する。 Jinjaの記法に自信がないうちは、こまめにコンパイルして結果を確認する癖をつけましょう。「JinjaはSQLを生成する道具」という感覚が自然と身につきます。

複雑にしすぎない。 Jinja2は強力ですが、過度に複雑なロジックをSQLファイルに詰め込むと、可読性が大きく低下します。「同僚がこのSQLを読んで理解できるか」を常に意識しましょう。

まとめ

この記事で紹介したJinja2の知識を整理します。

カテゴリ 覚えるべきこと
基本構文 {{ }}(出力)、{% %}(制御)、{# #}(コメント)
dbt関数 ref(), source(), config(), var()
制御構文 {% if %}, {% for %}, {% set %}
マクロ {% macro %} で定義し {{ }} で呼び出す
フィルター | で値を加工(upper, default, join など)
空白制御 - をタグ内側に付ける({%- -%}
デバッグ dbt compiletarget/compiled/ の確認

学習の優先順位としては、以下の順番がおすすめです。

  1. 3つの基本構文{{ }}{% %}{# #})の見分け方
  2. ref()source() の使い方
  3. {% if %}{% for %} による動的SQL生成
  4. マクロ による共通処理の切り出し

Jinja2はdbtの表現力を支える中核技術です。すべてを一度に覚える必要はありませんが、基本構文と ref() / source() を押さえるだけでもdbtの開発がぐっと楽になります。

最後まで読んでいただきありがとうございました。この記事が参考になりましたら、ぜひLGTMをお願いします。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?