dbt macro tips advent calendar 2022 day 24 - 時間関係
便利なデータ変換ツールである dbt の中のmacroに関するtipsを書いていく dbt macro tips Advent Calendar 2022 24日目です。
時刻
dbtでモデルを書いてる際、特にincrementalなモデルを書いていると、時刻関係を取り扱いたくなると思います。
例えば、データの遅延到着が発生したので、基準日から7日前の間を再処理したいとします。
var('current_datetime')
のように var functionを使うことで処理の基準日を指定することを考えたとします。
このとき、いくつか懸念点があります。
- 日付として正しいフォーマットの文字列が渡されるのか?
- 7日前の算出はどうやって実現したら良いのか?
- TimeZone関係は?
- etc...
もちろんひとつの簡単な方法として、run_query
を使ってDWHに問い合わせるという方法があります。
しかし、それでは時刻関係を取り扱いたいときに1クエリ実行することになり、とても非効率的です。
そんなときのために、dbtではmacro中でPythonのdatetime,pytz等の時刻関係のモジュールを取り扱えるようにするインタフェースが用意されています。
今回はそちらの使い方を見せつつ、便利なマクロをいくつか紹介します。
さて、何をするにも、現在時刻の取得というのが大事だと思います。
試しに現在時刻を取得するmacroを書いてみましょう。
{%- macro var_current_datetime() %}
{{ return(modules.datetime.datetime.now()) }}
{%- endmacro %}
modules.datetime
以下はPythonのdatetimeモジュールと同じ様になっています。
modules.datetime.datetime.now()
というのは datetimeモジュールのdatetime オブジェクトのクラスメソッドである now()
を使用しているわけですね。
now()
は通常ローカル時刻のdatetimeオブジェクト(TZ情報なし)を返します異なるTimeZoneでの現在時刻がほしいというケースなどを考えて pytzを利用した改造をしてみましょう。
{%- macro var_current_datetime(tz=none) %}
{%- set dt = modules.pytz.timezone('UTC').localize(modules.datetime.datetime.utcnow()) %}
{%- if tz is none %}
{{ return(dt) }}
{%- endif %}
{{ return(dt.astimezone(modules.pytz.timezone(tz))) }}
{%- endmacro %}
今度のmacroはUTCで返すのが基本となっています。
ためしに run-operation
コマンドを使って動作を確認してみましょう。
{%- macro main() %}
{% do log('UTC: ' ~ var_current_datetime(), info=True) %}
{% do log('JST: ' ~ var_current_datetime(tz='Asia/Tokyo'), info=True) %}
{%- endmacro %}
$ dbt run-operation main
03:52:29 Running with dbt=1.3.1
03:52:30 UTC: 2022-12-22 03:52:30.059808+00:00
03:52:30 JST: 2022-12-22 12:52:30.060650+09:00
このようになります。
さて、次は実行時に --var 'current_datetime: 2023-01-01T00:00:00+0900'
のように実行時に値を渡せるようにしてみます。
{%- macro var_current_datetime(tz=none) %}
{%- set dt_str = var('current_datetime', '') %}
{%- if dt_str != '' %}
{%- set dt = modules.datetime.datetime.strptime((dt_str | string), '%Y-%m-%dT%H:%M:%S%z').astimezone(modules.pytz.timezone('UTC')) %}
{%- else %}
{%- set dt = modules.pytz.timezone('UTC').localize(modules.datetime.datetime.utcnow()) %}
{%- endif %}
{%- if tz is none %}
{{ return(dt) }}
{%- endif %}
{{ return(dt.astimezone(modules.pytz.timezone(tz))) }}
{%- endmacro %}
$ dbt run-operation main --var 'current_datetime: 2023-01-01T00:00:00+0900'
05:29:10 Running with dbt=1.3.1
05:29:11 Unable to do partial parsing because config vars, config profile, or config target have changed
05:29:14 UTC: 2022-12-31 15:00:00+00:00
05:29:14 JST: 2023-01-01 00:00:00+09:00
$ dbt run-operation main --var 'current_datetime: invalid'
05:32:59 Running with dbt=1.3.1
05:33:00 Unable to do partial parsing because config vars, config profile, or config target have changed
05:33:03 Encountered an error while running operation: Runtime Error
time data 'invalid' does not match format '%Y-%m-%dT%H:%M:%S%z'
コレで変えることができましたね。実行時に好きなように時間を指定できるようになりました。
さて、任意の時刻のdatetimeオブジェクトを取得できるようになりましたので、あとは3日前や指定のフォーマットでの出力になります。
mainのoperationを改造して、例を示してみます。
{%- macro main() %}
{% do log('UTC: ' ~ var_current_datetime().strftime('%Y-%m-%d'), info=True) %}
{% do log('JST: ' ~ var_current_datetime(tz='Asia/Tokyo').strftime('%Y-%m-%d'), info=True) %}
{% do log('Before 7 day: ' ~ (var_current_datetime(tz='Asia/Tokyo')-modules.datetime.timedelta(days=7)).strftime('%Y-%m-%d'), info=True) %}
{%- endmacro %}
$ dbt run-operation main --var 'current_datetime: 2023-01-01T00:00:00+0900'
05:42:18 Running with dbt=1.3.1
05:42:19 UTC: 2022-12-31
05:42:19 JST: 2023-01-01
05:42:19 Before 7 day: 2022-12-25
strftimeやtimedeltaを使うことで日付の計算や文字列化ができます。
この辺の使い方を迷ったらPythonのドキュメントを見にいくようにしましょう。
25日目は何にしようかまだ悩んでいます。
Discussion