🦁

dbt macro tips advent calendar 2022 day 4 - 制御構文

2022/12/04に公開

便利なデータ変換ツールである dbt の中のmacroに関するtipsを書いていく dbt macro tips Advent Calendar 2022 4日目です。

制御構文

macroは引数を取ることで、実際に描画される内容を変更することができるようになりました。
こうなると、もっとダイナミックに描画内容を書き換えたくなると思います。
それを実現するのが制御構文です。
制御構文として代表的なものが 条件分岐if と 反復for となります。

さて、iffor の前に一つ大事なことを押さえたいと思います。
それは、jinjaのテンプレートで使われるmacroは上から下に解釈されるということです。
他の汎用プログラミング言語を使用している人にとっては、「なにを当たり前な!?」と思うでしょう。
そうです、当たり前なことなのですが、制御構文の話をするときにはコレもちゃんと押さえておくのは大事だと思います。

条件分岐 if

与えられた引数の中身によって、描画内容を切り替えたい時があると思います。
materializationのincremental を使ったことがある方は、お気づきだとは思いますが、 if を使います。

https://docs.getdbt.com/docs/build/incremental-models

上記のincrementalモデルの説明に登場するsqlを取り上げます。

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

select
    *,
    my_slow_function(my_column)

from raw_app_data.events

{% if is_incremental() %}

  -- this filter will only be applied on an incremental run
  where event_time > (select max(event_time) from {{ this }})

{% endif %}

ここで、 {% if is_incremental() %} ... {% endif %} という記述があります。
これが、if の構文になります。
※実は、is_incremental() というのは、dbtのシステムがデフォルトで用意されているmacroだったのです。このmacroは2回目以降の実行かどうかを返します。

さて、このif の説明をする時にセットになってくるのが比較演算子の話です。
if の記述の次には true/false という真偽値を取るものが来るのですが、通常使う場合は引数や後日説明する変数の中身との比較の結果で分岐したいので、比較演算子というものを使って比較の結果をtrue/falseで取得することになります。

具体的な比較演算子は、 Template Designer Documentation #comparisons
を参照してもらうとして、ここでは 簡単な if を使った例を取り上げます。

macro/noop.sql
{%- macro noop(msg='') %}
{%- if msg != '' %}
-- {{ msg }}
{%- endif %}
{%- endmacro %}

とても単純です。 msgの中身が空でなければ、なにかSQLコメントを描画するという内容になります。

反復 for

こちらは、varargs や kwargs のようなtupleやdictのような引数に足して、各要素に関して何かしらの処理を行うというものになります。

長々と使い方を語ってもよいのですが、若干煩雑になりますので具体的な説明は Template Designer Documentation #forを参照してもらうとして、varargskwargsforの組み合わせの例を示そうと思います。

macro/constant_records.sql
{%- macro constant_records() %}
{%- for record in varargs %}
  select {{ constant_record(**record) }}
  {% if not loop.last %}union all{%- endif %}
{%- endfor %}
{%- endmacro %}


{%- macro constant_record() %}
{%- for key, value in kwargs | dictsort -%}
  {{ value }} as {{ key }}
  {%- if not loop.last %}, {%- endif %}
{%- endfor %}
{%- endmacro %}

このmacroを使って、最初の my_first_dbt_modelを書き換えることができます。

models/example/my_first_dbt_model.sql

/*
    Welcome to your first dbt model!
    Did you know that you can also configure models directly within SQL files?
    This will override configurations stated in dbt_project.yml

    Try changing "table" to "view" below
*/

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

with source_data as (
    {{ constant_records(
        {'id': 1, 'name': "'hoge'"},
        {'id': 'null', 'name': "'fuga'"}
    )}}
)

select *
from source_data

/*
    Uncomment the line below to remove records with null `id` values
*/

-- where id is not null

描画結果は次のようになる。

target/compiled/macro_tips_advcal/models/example/my_first_dbt_model.sql
/*
    Welcome to your first dbt model!
    Did you know that you can also configure models directly within SQL files?
    This will override configurations stated in dbt_project.yml

    Try changing "table" to "view" below
*/



with source_data as (
    
  select 1 as id,'hoge' as name
  union all
  select null as id,'fuga' as name
  
)

select *
from source_data

/*
    Uncomment the line below to remove records with null `id` values
*/

-- where id is not null% 

もちろん、この例はちょっと謎のカッコよさがあるだけで、実用的ではありません。
しかし、複雑なmacroを組むときにはこのエッセンスが必要になることもあるので覚えておいて損はありません。

ところで、macro中に {%- if not loop.last %}, {%- endif %} という記述がありました。
これはdbtでforを使う場合に非常によく使う記述です。覚えておくと良いでしょう。forの反復が最後でない場合は、if の中身を描画するということになります。ここで出てくる loop というのは forendfor の間でしか使えない特殊な変数となります。


5日目は変数に関する説明になります。 5日目の内容でmacroに関する基礎的な内容は終了になります。そろそろより濃厚なtipsに突っ込んでいく時期が近づいてまいりました。

Discussion