はじめに
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 やコマンドラインで定義した変数を参照します。環境ごとに値を切り替えたい場合に便利です。
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 ファイルとして配置します。
{% macro cents_to_dollars(column_name, precision=2) %}
round(cast({{ column_name }} as numeric) / 100, {{ precision }})
{% endmacro %}
モデルからは {{ }} で呼び出します。
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か所で済みます。
{% macro date_trunc(datepart, column) %}
{% if target.type == 'snowflake' %}
date_trunc('{{ datepart }}', {{ column }})
{% elif target.type == 'bigquery' %}
date_trunc({{ column }}, {{ datepart }})
{% endif %}
{% endmacro %}
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に意図しない空行やインデントの乱れがある場合は、以下の手順で対処します。
-
dbt compileを実行し、target/compiled/のSQLを確認する - 空白が気になる箇所の
{% %}に-を付けてみる - 再度
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 compile と target/compiled/ の確認 |
学習の優先順位としては、以下の順番がおすすめです。
-
3つの基本構文(
{{ }}、{% %}、{# #})の見分け方 -
ref()とsource()の使い方 -
{% if %}と{% for %}による動的SQL生成 - マクロ による共通処理の切り出し
Jinja2はdbtの表現力を支える中核技術です。すべてを一度に覚える必要はありませんが、基本構文と ref() / source() を押さえるだけでもdbtの開発がぐっと楽になります。
最後まで読んでいただきありがとうございました。この記事が参考になりましたら、ぜひLGTMをお願いします。