ふくふくHukuhuku Inc.
EP.08dbt 11分公開: 2026-05-10

macros:Jinja で SQL を再利用する

同じパターンの SQL を10個書きそうになったら macro を書こう。Jinja の基本構文と、現場で繰り返し作る代表的な macro 5 つ (PII マスキング / 期間集計 / pivot 等) を整理。

#dbt#macros#Jinja
シェア

macros/ ディレクトリに置いた Jinja マクロは、`macro_name()` で の中から呼び出せる 再利用可能な SQL 部品です。「同じパターンを 10 個書きそう」になったら macro 化のサイン。

Jinja の基本構文

構文意味
`...`値を SQL に展開`ref('orders')`
`{% ... %}`ロジック (if/for/set 等)`{% if is_incremental() %}`
`{# ... #}`コメント (SQL に出ない)`{# TODO: tier 追加 #}`
`var('x')`プロジェクト変数`var('start_date')`
`env_var('X')`環境変数`env_var('DBT_PROFILE')`

macro 1: マスキング

macros/mask_pii.sql
SQL
{% macro mask_email(column_name) %}  CASE    WHEN {{ var('mask_pii', false) }}    THEN regexp_replace({{ column_name }}, '.+@', '***@')    ELSE {{ column_name }}  END{% endmacro %}
{% macro mask_phone(column_name) %}  CASE    WHEN {{ var('mask_pii', false) }}    THEN concat(left({{ column_name }}, 3), '-****-****')    ELSE {{ column_name }}  END{% endmacro %}
model から呼び出し
SQL
SELECT  customer_id,  {{ mask_email('email') }} AS email,  {{ mask_phone('phone') }} AS phoneFROM {{ ref('stg_customers') }}
実行時に var で切替
Bash
# 開発: マスクなしdbt run --select dim_customers
# QA: マスクありdbt run --select dim_customers --vars '{"mask_pii": true}'

macro 2: 月次集計テンプレ

macros/monthly_aggregation.sql
SQL
{% macro monthly_agg(table, date_col, group_cols, measures) %}  SELECT    DATE_TRUNC({{ date_col }}, MONTH) AS month,    {% for c in group_cols %}{{ c }},{% endfor %}    {% for m in measures %}      {{ m.func }}({{ m.col }}) AS {{ m.alias }}{% if not loop.last %},{% endif %}    {% endfor %}  FROM {{ table }}  GROUP BY 1{% for c in group_cols %}, {{ loop.index + 1 }}{% endfor %}{% endmacro %}
使う側
SQL
{{ monthly_agg(    table=ref('fct_orders'),    date_col='ordered_at',    group_cols=['region', 'category'],    measures=[      {'func': 'SUM', 'col': 'amount', 'alias': 'revenue'},      {'func': 'COUNT', 'col': '*',     'alias': 'order_count'},      {'func': 'COUNT', 'col': 'DISTINCT customer_id', 'alias': 'unique_customers'},    ]) }}

macro 3: 動的 pivot

dbt_utils.pivot を使う
SQL
-- カテゴリのリストを動的取得して pivot{% set categories = dbt_utils.get_column_values(    table=ref('fct_orders'),    column='category') %}
SELECT  customer_id,  {{ dbt_utils.pivot(      'category',      categories,      agg='sum',      then_value='amount',      else_value=0,      prefix='revenue_'  ) }}FROM {{ ref('fct_orders') }}GROUP BY customer_id

macro 4: 環境別の挙動切替

macros/limit_dev.sql
SQL
{% macro limit_dev(rows=1000) %}  {% if target.name == 'dev' %}    LIMIT {{ rows }}  {% endif %}{% endmacro %}
model で使う
SQL
SELECT * FROM {{ source('events', 'raw') }}{{ limit_dev(rows=10000) }}-- dev では LIMIT 10000、prod ではフル

macro 5: pre/post-hook で監査ログ

macros/log_run.sql
SQL
{% macro log_run_start(model_name) %}  INSERT INTO meta.dbt_run_log (model, started_at, target)  VALUES ('{{ model_name }}', CURRENT_TIMESTAMP(), '{{ target.name }}'){% endmacro %}
{% macro log_run_end(model_name) %}  UPDATE meta.dbt_run_log  SET ended_at = CURRENT_TIMESTAMP()  WHERE model = '{{ model_name }}' AND ended_at IS NULL{% endmacro %}
dbt_project.yml で全モデルに適用
YAML
models:  analytics:    +pre-hook: "{{ log_run_start(this.name) }}"    +post-hook: "{{ log_run_end(this.name) }}"

macro デザインのコツ

  • 複雑になりすぎたら model に戻す ── マクロの中に 50 行を超える は読めない
  • 引数のデフォルト値を必ず設定 ── 呼び出し側を簡潔に
  • `{% if execute %}` で `dbt parse` 時の挙動を抑制 (副作用あるクエリ)
  • dbt_utils を真似ない ── パッケージ既存のものは再発明しない (EP.09)
  • docstring を書く ── 引数と返り値を {% docs %} で

次の話

EP.09 では `dbt_utils` `dbt-expectations` などの定番パッケージを扱います。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp