痒いところに手が届いて助かったdbt test
はじめに
この記事は、dbt Advent Calendar 2025 4日目の記事です。
これまで複数のdbt modelで構成されるデータパイプラインを3年以上運用してきました。dbt testは種類も多くなんとなく便利そうくらいなものが多いですが、この記事ではdbt testについてというより、「実運用でこういうケースのときに、このdbt testを入れておくべきだった/入れておいて助かった」という感じでユースケースを中心に紹介できればと思います。
データウェアハウスとしてBigQueryを利用しているケースでお話しますが他のデータウェアハウスでも大丈夫かと思います。
異なるデータソースの統合(UNION ALL)エラーの検知
ユースケース: システムの移行に伴うdbt modelの修正
dbt modelで利用している元のデータソースがアプリケーションの移行に伴い変更されるケースは実運用だとよく起こります。たとえば、アプリケーションAがdeprecatedになり、これからはアプリケーションBのデータを使ってくださいというケースがあります。
多くの場合でデータ活用ではアプリケーションAの過去データも引き続き活用したいのでDWH上は維持した上で、変更時点からアプリケーションBのデータをdbt model側で使うように変更する対応が必要になります。この場合BIツール等ですでにdbt modelが色んなところで利用されているため新規model作成したくはなく、データ統合UNION ALLして対応することが多いです。
つまり、もともとは以下の運用をしていたものが、
このように変わりました。
martのSQL内は以下のようにUNION ALLすればいいだけで大きな問題は起こらそうだなとそう思っていました。
SELECT data_source, session_key, device_id, user_id, event_time, .... FROM itm_A
UNION ALL
SELECT data_source, session_key, device_id, user_id, event_time, .... FROM itm_B
そして移行を迎え、無事にitm_Bからのデータが統合されて数週間くらい問題も起こっていませんでしたが、ある日分析担当者から「なんかmartテーブルのuser_idって変わりました?JOINしたら0件になっちゃいまして....」という連絡が。martテーブルはレコード件数もデータの中身もパッと見大丈夫そうだったのですが、SQLをよくみると以下のとおりで「device_idとuser_idの順序変わってしまってる....」となりました。STRING型同士でUNIONエラーでてなかったというオチでした...
SELECT data_source, session_key, device_id, user_id, event_time, .... FROM itm_A
UNION ALL
SELECT data_source, session_key, user_id, device_id, event_time, .... FROM itm_B
対処法: relationshipstestをちゃんといれよう
relationshipテストわざわざ入れるケースはあまり多くないかなと思いますが、このようになんらかの形でuser_idに意図しない別のカラムからの値が混ざるケースがあります。UNION ALLをする場面では以下を設定してusersマスターにはないidが入ると検知できるようにしておくとよいでしょう。
- name: user_id
description: "ユーザーID"
data_tests:
- not_null
- relationships:
arguments:
to: ref('user_db__users')
field: id
ちなみにBigQueryでは今年に入ってBY NAMEがリリースされているのでこれを使えばよいかもですが、カラム順が揃ってないまま運用するのはお気持ちとして気持ち悪いのもあり実際はあまり使えてはいません。アドホックにSQLを書く時は重宝しそうです。
非NULL割合のエラー検知
ユースケース: 品質の低い旧イベントを含むデータソースでの分析
イベントログなどフロントエンドで取得するデータの場合、NULL許容カラムのスキーマが設定されていることが多いです。イベントによっては最新の実装で新規追加する場合は非NULLの値が入り、また一方古いイベントではNULLが一定割合入ってしまうようなケースがあります(負債も重なって調査・修正が難しい場合もあります)
このときあるイベントでは非NULLとするようなdbt testを書いてもよいですが、A/Bテスト等で短いスパンで新しいイベントを追加する場合に毎回dbt testの条件を更新していくのはあまり現実的ではありません。また非NULLの値が入ることをフロントエンドでのイベントログでは完全に保証するのも難しいです。このような状況でゆるやかに非NULL割合の異常を検知してあまりにNULLが増えている時にのみ対応したいといったことがあります。
対処法: not_null_proportionと期間指定でゆるやなか異常を検知しよう
dbt-utilsには not_null_proportion という非NULL割合の閾値を定めてその割合を下回ったときに検知してくれるテストを書くことができます。また期間を例のように直近期間で指定することで、直近の実装による変化でログへの影響が大きく変わったかどうかを検知できます。
実際の実運用では滅多にエラーが起こることはなかったのですが、この1年くらいで数件検知され迅速に気づくことができて対応できたこともあり、色んなケースに対応しておくのは重要だなと改めて感じました。
- name: user_id
description: "ユーザーID"
data_tests:
- dbt_utils.not_null_proportion:
config:
where: "DATE(event_time) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY) AND DATE_ADD(CURRENT_DATE(), INTERVAL 1 DAY)"
arguments:
at_least: 0.9
データの存在期間のエラー検知
ユースケース: 過去データがオペレーションミス等でいつのまにか消失
長くデータを運用していると、システム側のデータ操作やオペレーションミスによってデータウェアハウス側の一部の過去データが誤って削除されてしまうケースがあります。アプリケーション側のデータソースなどで復旧は可能ですがヒヤッとする場面があります。
ややこしいことに全データが消えるのではなく、過去数ヶ月間は残っていてある日以前から消えてしまうといったこともあり、普段の分析では気づかずあるときに全期間の統計値を出すときに「あれ?なんか年間UU数少なくない?」と気づくことがあります。
たとえば、2022年から現時点までのデータがあるテーブルにはあると期待してたのですが、
実際はいつのまにか以下のようになっていたというケースです。
滅多に起こらないエラーだからこそ、必要なケースだいたい急ぎで必要となる事が多いので、滅多に起こらないことでもちゃんと迅速に検知できるようにしておきたいと当時思いました。
対処法: expect_column_min_to_be_between 過去データの期間を設定しよう
dbt-expectationsにはexpect_column_min_to_be_between というdbt testがありカラムの最小値がある区間のなかに入っていることを保証するようなテストが書けます。これを応用して以下のようにテストを指定します。
- name: event_time
description: "ログを送信時刻"
data_tests:
- dbt_expectations.expect_column_min_to_be_between:
arguments:
max_value: "TIMESTAMP('2022-01-02')"
row_condition: "event_time <= CURRENT_TIMESTAMP()"
今回のケースではイベント発生時刻の最小値が2022-01-02より前かかどうかを上記のように記載することができます。ただ前提として、データ消失は中途半端に間のある期間だけ起こるということはなく、ある日以前のものがまとまって消失される場合にこれで検知できます。実際これが活躍したケースは幸いにしても今のところ起こっていませんが、またいつか設定しておいてよかったと思う日が来るかもしれませんし、そうじゃないかもしれません。
(もう少しスマートな書き方があるかもしれませんし、custom generic testを書いたほうがわかりやすいかもしれません。ぜひより良い方法についてFBいただけると嬉しいです)
おわりに
dbt testをとりあえず設定しておくかということも大事ですが、コストもかかりますし、なぜ必要なのか?普段の業務でどういうエラーケースのときにこのテストが役立ちそうか?の解像度を高めてからdbt testを設定するのも大事かと思います。データ品質を守っていきましょう。
Discussion