📘

データ品質を支えるdbt test ~Ubieの事例を添えて~

2022/12/05に公開

はじめに

こんにちは。おきゆきです。現職のUbieでは、Data Analyst / Analytics Engineerとして働いています。(自己紹介ページTwitterはこちらです)

この記事は、dbt Advent Calendar 2022 5日目の記事 / Ubie Engineers & Designers Advent Calendar 2022 5日目の記事です。2つのアドベントカレンダーに関わる dbt×Ubieということで、Ubieの圧倒的なデータ品質を支えるdbt testの事例を紹介します。最近同僚の @jagabass から、Ubieでのデータ品質の重要性とdbtを用いたデータマネジメント領域のさまざまな課題解決事例が紹介されました。この記事は、より具体的なdbt testの例になるかと思います。

https://speakerdeck.com/skntbass/detabizinesuniokerushi-jian-de-dbthuo-yong-li

上記スライドで書いてあるUbieでのデータ品質の重要性を端的にいうと、

  • 希少疾患の患者発見プロジェクトにおいて、一人ひとりのユーザが極めて大事
  • 顧客との信頼関係構築において、顧客向けのレポーティング品質が極めて大事

といった点です。データがビジネスのコアである我々の事業において、適切なデータを提供するための強力なHowの1つがdbtです。データ品質の重要性うんぬんもありますが、1エンジニアとして、シンプルで良いdbt testが書けたときはとても気持ちが良いので、自分はdbt testを考える時間がとても好きです。

今回紹介する事例は、dbt coreで提供されるtest以外にも、dbt-utilsdbt-expectationsの各種パッケージで利用できるGeneric testsも利用します。また前提として、UbieではデータウェアハウスとしてBigQueryを利用しています。

ケース1: 特定の条件下で非NULLかtestしたい

dbt coreでは 個々のGeneric testの中でwhere句を使用することができます。これにより、where句で条件を満たすレコードに絞って、testを行うことができます。

https://docs.getdbt.com/reference/resource-configs/where

ユースケースとしては、過去からの全レコードを保持するeventテーブルで、特定のカラムはnot_nullを保証したいのですが、リリース当初や特定の期間は開発時のバグ等で、NULL値が入ってしまうようなケースがあります。

また、別のユースケースとして、患者発見プロジェクトを管理するマスターデータは、オペレーションの都合により、プロジェクトに関するメタデータが人手で入力されます。そのため、testを行う時期によっては、まだデータが入っておらず、本来not_nullを保証したいカラムにNULL値が入ってしまう問題がありました。

それを対処するために、not_nullを保証する正確な期間、または必要な期間にだけtestを行っています。

models:
  - name: sample_events
    description: イベントテーブルです
    columns:
      - name: event_time
        description: "イベント時刻"        
      - name: event_key
        description: "イベントキー"
        tests:
          - not_null:
              config:
                # 2022-02-28以前は、バグA (チケットURL: xxx) により、NULLが入ってしまっていたので、2022-03-01以降は非NULL
                where: "DATE(event_time) >= '2022-03-01'"
models:
  - name: project_master
    description: プロジェクト管理マスタテーブルです
    columns:
      - name: project_id
        description: "プロジェクトID"        
      - name: project_start_date
        description: "プロジェクト開始日"
      - name: project_end_date
        description: "プロジェクト終了日"
      - name: project_target_disease
        description: "プロジェクト対象の疾患"
        tests:
          - not_null:
              config:
                # 当日がプロジェクト期間(プロジェクト開始日 ~ 終了日)中に該当するときは非NULL
                where: "CURRENT_DATE() BETWEEN DATE(project_start_date) AND DATE(project_end_date)"

すべてのレコードで非NULLとなるようなテーブル設計、またオペレーション設計も大事ですが、レポーティング品質を毀損しないような柔軟な条件を設定することで、問題の早期対処・検知が可能となります。

また、これらはnot_nullだけでなく、accepted_values testにも応用できます。

ケース2: 意図した数値範囲のデータになっているかtestしたい

ビジネスロジックや機械学習の特徴量変換により、レポーティングで含める数値範囲が意図どおりになっているかtestしたいケースがあります。dbt_utils.accepted_rangeならそのようなtestを容易に実行することができます。

https://github.com/dbt-labs/dbt-utils#accepted_range-source

もちろん整数で10個くらいなら、dbt-coreのaccepted_valuesを使えばよいですが、float型や下限値だけ制約があるケースなどでも、以下のように柔軟に対処できます。

models:
  - name: feature_store
    description: 特徴量保存テーブルです
    columns:
      - name: feature_numeric_column_a
        description: "正規化済みイベントA"
        tests:
          - dbt_utils.accepted_range:
              min_value: 0.0
              max_value: 1.0
      - name: feature_categorical_column_b
        description: "カテゴリカルイベントB"
        tests:
          - dbt_utils.accepted_range:
              min_value: 0
              max_value: 1023
              inclusive: true # = を含む
      - name: feature_cnt_c
        description: "イベントCの発生回数"
        tests:
          - dbt_utils.accepted_range:
              min_value: 0
              inclusive: false

ケース3: 複数カラムでuniqueかtestしたい

テーブル設計において、not_null&uniqueな主キーとなるカラムを作成するのは基本かと思います。また、主キーの変わりに、代理キー(サロゲートキー)を生成することもHowの1つです。

https://docs.getdbt.com/blog/sql-surrogate-keys

一方、代理キーとして専用のカラムを生成しなくても、 dbt_utils.unique_combination_of_columns を利用すれば、複数カラムでunique判定が可能になります。

https://github.com/dbt-labs/dbt-utils#unique_combination_of_columns-source

代理キーの有無に関わらず、明示的にdbt_utils.unique_combination_of_columnsを行うことで、そのテーブルを初めて触る人が、テーブルの構造をschema.ymlから読み取ることが容易になります。この記事の冒頭で紹介した@jagabassの資料内の「分析者向けドキュメントとしてdbt testを設定している」と同じ話です。

以下は、月次のプロジェクト別のレポート項目ごとのメトリック値を保存するテーブルです。schema.ymlをみればひと目で、「このテーブルは月次×プロジェクトID×レポーティング項目ごとにユニークな値が入ってるテーブルなんだな」と理解できます。

models:
  - name: monthly_performance_reports
    description: 月次の品質レポーティングマートです
    tests:
            - dbt_utils.unique_combination_of_columns:
          combination_of_columns:
            - monthly
            - project_id
            - reporting_items
    columns:
      - name: monthly
        description: 月次
      - name: project_id
        description: プロジェクトID
      - name: reporting_items
        description: レポーティング項目
      - name: metric_value
        description: 対応するレポーティング項目の数値

ケース4: データが任意のlike句や正規表現とマッチしているかtestしたい

人が手入力したマスタデータを扱うこともあり、入力するUI上でデータがバリデーションされないこともあるため、入力値が意図通りかのdbt testを行っています。

また、特定のID系統が事前に定義した命名規則どおりか検知し、問題があれば迅速に修正するためのトリガーとしてdbt testを利用しています。

いろいろな書き方がありますが、ここでは dbt_expectations.expect_column_values_to_match_like_pattern_list を使った例を紹介します。

https://github.com/calogica/dbt-expectations#expect_column_values_to_match_like_pattern_list

models:
  - name: project_option_entry
    description: プロジェクトの設定項目
    columns:
      - name: project_id
        description: "プロジェクトID"        
      - name: project_option_id
        description: "プロジェクトオプションID"        
      - name: project_option_value
        description: "プロジェクトオプション値"
        tests:
          # 空値 or [] で挟まれた文字列のどちらかにマッチすればデータ上問題なし
          - dbt_expectations.expect_column_values_to_match_like_pattern_list:
              like_pattern_list: ["", "[%]"]
              match_on: any # like_pattern_listのいずれかにマッチすれば良い
              row_condition: "project_option_id = 'XXX'"

dbt-expectationsには複数のtestが用意されており、READMEを読むだけでもとても勉強になります。

ケース5: その他任意の条件を満たしているかtestしたい

特定のカラムの値はNULL値であることが逆に正しかったり、ARRAYのカラムのtestは、dbt coreのGeneric testだけでは意図したtestを実装するのが難しかったりします。そのため、より拡張性の高いtestを行うために、 dbt_utils.expression_is_trueを使用しています。

https://github.com/dbt-labs/dbt-utils#expression_is_true-source

models:
  - name: all_events
    description: すべてのイベントを結合したテーブルです
    columns:
      - name: event_time
        description: "イベント時刻"        
      - name: event_key
        description: "イベントキー"
      - name: event_type
        description: "イベントタイプ"
      - name: event_parameter_1
        description: "イベントパラメータ(ARRAY<STRING>)" 
      - name: event_parameter_2
        description: "イベントパラメータ"
        tests:
          # イベントタイプaとbのときはXXXの理由で、非NULLとは限らないためnot_nullテストは省略
          # 一方、イベントタイプcとdのときは、YYYの理由で、このパラメータはNULLになる
          - dbt_utils.expression_is_true:
              expression: 'is null'
              condition: 
                "event_type in (
                  'type_c',
                  'type_d'
                )"
    tests:
      # イベントパラメータが必ず1つ以上の要素を含んでいる
      - dbt_utils.expression_is_true:
          expression: 'array_length(event_parameter_1) > 0'

dbt_utils.expression_is_true は柔軟に条件を書くことができるので、これまでのケースのテストを置き換えたり、複雑な条件のtestも行えるかと思います。

参考とした事例

おわりに

今回はGeneric testを中心に、dbt×Ubieの事例を紹介しました。dbtは 非常にシンプルな記述で、正確性を担保するためのtestを柔軟に行うことができる と感じていただければ嬉しいです。また、1つでも「自社のあのデータで使えそう!」と感じていただければ幸いです。今回の事例以外にも、Ubieではデータ品質を維持するための取り組みを、各社のdbt事例も参考に継続して行っています。

来年もdbtを中心とした各社のデータ基盤の発展を見聞きするのが楽しみですね。それではまた!

Discussion