dbtでrunの前にtestを実行したい場合にephemeralを使う

dbt運用の小技です。品質の高いデータをユーザーに提供するために一工夫が必要だったので、メモしておきます。

課題感: testを行なってからrunしたい

dbtはSQLやデータに対してテスト(dbt test)を行なうことができます。データ品質を高めていく上で、重要な機能の一つです。

テーブルやビューに対してテストを行なうわけですが、当然ながら素朴にやるとテーブルやビューがまだ存在していない場合にはテストを実行することができません。無理やり実行すると、こういう感じで失敗します。

Completed with 4 errors and 0 warnings:

Runtime Error in test unique_my_first_dbt_model_id (models/example/schema.yml)
  404 Not found: Table ml-project:test_test.my_first_dbt_model was not found in location asia-northeast1

  (job ID: 106ee870-a21a-4059-8b4f-9be28ad0b6da)

Runtime Error in test not_null_my_second_dbt_model_id (models/example/schema.yml)
  404 Not found: Table ml-project:test_test.my_first_dbt_model was not found in location asia-northeast1

  (job ID: 15eb062b-c496-42f7-a53d-15cf277243e0)

「品質が保証されているデータのみをデータユーザーに提供したい」という場合、dbt testを実行してからdbt runを実行したい(つまり、testがこけたらrunすらして欲しくない。間違ったデータをユーザーが利用する状況を起こしたくない)というケースが運用をやっていると出てきました。参考までにですが、自社ではDWHのバージョン管理などを定義して、ちゃんと運用していきたいね、ということをやり始めている最中です。

補足: 正確性と可用性のトレードオフ

「一部に正確性が欠けることがあっても、ひとまずテーブルとしては提供したい」というケースももちろんあると思います。正確性と可用性はトレードオフの関係にあることが多いので、自分のケースがどちらにより当てはまるかは考える必要があるでしょう。より詳しいことを知りたい人は、以下の本のChapter 18を読んでみるとよいと思います。

ephemeralをうまく使う

運用上どうするかはちょっと悩みました。「テスト用のデータセットを切って、そちらで一回テストをやって、通ったら本番用のtableにcopyして...」とかも考えましたが、管理するものが増えるし、オペレーションも煩雑になる。「dbtならきっといい感じにやる方法が何かあるはず〜」と思って(祈って)調べてみたところ、テーブルやビューではなく、ephemeralを使うとうまく回避できそうでした。

ephemeralは実際に何かを作ることはなく、WITH句のようなCTEとして処理されます。具体的に見ていきましょう。

dbt testを走らせると、テストはtarget/compiled/my_new_project/models/example/schema.yml/schema_test/not_null_my_first_dbt_model_id.sqlのようにSQLファイルとして実行されます。SQLの中身はこういう感じ。存在していないテーブルやビューが対象になっているため、テストが落ちます。

select *
from `ml-project`.`test_test`.`my_first_dbt_model` -- こいつはまだ存在しない
where id is null

ephemeralを使うと中身を展開してくれます。なので、テストをしたい対象のテーブルやビューがまだ存在しなくても、中身のSQLをWITH句の中で展開してくれて、テストを実行できるというわけです。中間テーブルが多段に依存関係があって...というような場合もWITH句を複数用意してくれるので、この辺はさすがdbtという感じでした。

with __dbt__cte__my_first_dbt_model as (
  SELECT
  ...
  -- my_first_dbt_modelの中身のSQLがWITH句の中で展開してくれる
)

select *
from __dbt__cte__my_first_dbt_model
where id is null

言いたいことはこれだけっちゃこれだけなのですが、本来不要なリソースの生成を避けるという意味でノウハウだなと思ったので、メモとして残しておきます。