目次
本記事は、dbtについて調べたことを自分なりに整理することを目的としました。
長めの記事になりましたので、一応、目次を作りました!
dbtベストプラクティスについて
Model contractsについて
環境
コードの動作環境は下記の通りです。
$ dbt -V
Core:
- installed: 1.9.2
- latest: 1.9.2 - Up to date!
Plugins:
- postgres: 1.9.0 - Up to date!
$ psql -V
psql (PostgreSQL) 14.15 (Ubuntu 14.15-0ubuntu0.22.04.1)
dbtベストプラクティスについて
dbtを使ったデータモデリングの記事で利用したプロジェクトをベースに、フォルダ構成等をdbtのベストプラクティスに寄せていきたいと思います。
・フォルダ構成
公式のベストプラクティスに基づいて再構成したのがこちらです。
.
├── dbt_packages
├── dbt_project.yml
├── logs
├── macros
├── models
│ ├── intermediates
│ │ └── marketing
│ │ ├── _int_marketing__models.yml
│ │ ├── int_dim_customers.sql
│ │ ├── int_dim_dates.sql
│ │ ├── int_dim_products.sql
│ │ └── int_fact_sales.sql
│ ├── marts
│ │ └── marketing
│ │ ├── _marketing__models.yml
│ │ └── obt_sales.sql
│ └── staging
│ └── adventureworks
│ ├── _adventureworks__models.yml
│ ├── _adventureworks__sources.yml
│ ├── stg_adventureworks__stg_addresses.sql
│ ├── stg_adventureworks__stg_customers.sql
│ ├── stg_adventureworks__stg_dates.sql
│ ├── stg_adventureworks__stg_order_details.sql
│ ├── stg_adventureworks__stg_order_headers.sql
│ └── stg_adventureworks__stg_products.sql
├── seeds
│ └── adventureworks
│ ├── _adventureworks__seed.yml
│ ├── raw_addresses.csv
│ ├── raw_customers.csv
│ ├── raw_dates.csv
│ ├── raw_order_details.csv
│ ├── raw_order_headers.csv
│ └── raw_products.csv
├── target
├── tests
│ └── generic
│ └── validate_line_total_test.sql
・seed層
├── seeds
│ └── adventureworks
│ ├── _adventureworks__seed.yml
│ ├── raw_addresses.csv
│ ├── raw_customers.csv
│ ├── raw_dates.csv
│ ├── raw_order_details.csv
│ ├── raw_order_headers.csv
│ └── raw_products.csv
seed
で今回扱うすべてのデータをロードしています。
ただし本来は下記の用途で利用することが推奨されています。
モデリングには役立つが、どのソース システムにも存在しないルックアップテーブルを読み込むことです。
ソースデータ単位でサブフォルダを切っています。
これはstaging
レイヤのベストプラクティスを参考にしていますが、それは後述します。
・staging
│ └── staging
│ └── adventureworks
│ ├── _adventureworks__models.yml
│ ├── _adventureworks__sources.yml
│ ├── stg_adventureworks__stg_addresses.sql
│ ├── stg_adventureworks__stg_customers.sql
│ ├── stg_adventureworks__stg_dates.sql
│ ├── stg_adventureworks__stg_order_details.sql
│ ├── stg_adventureworks__stg_order_headers.sql
│ └── stg_adventureworks__stg_products.sql
サブディレクトリ
staging
レイヤでは、データソースに基づいてサブディレクトリを構成することが推奨されています。
今回はadventureworks
というシステムから抽出されたデータと仮定して構成しています。
この構造により、--select
構文で簡単にデータソース単位でのモデルを実行できます。
dbt build --select path:models/staging/adventureworks/*
命名規則
stg_[source]__[table名]s.sql
ファイル名の規則が推奨されています。
[source]
毎にサブディレクトリに分割します。
このことにより、データソースが増えてきても、モデルがどのソースから構成されているのか、一目でわかるようになります。
yaml
_[directory]__models.yml
yamlファイルはモデルフォルダ内にディレクトリごとに作成することが推奨されています。
またステージングフォルダ内では、_[directory]__sources.yml
もそれぞれのディレクトリ内に含めます。
先頭にアンダースコアを付けることで、YAMLファイルが各フォルダーの先頭に並べ替えられ、モデルファイルと簡単に見分けがつきます。
そのほかの推奨事項
-
ビューとしてマテリアライズする。
-
staging
モデルはソースマクロを唯一の場所であり、ステージングモデルはソーステーブルと1対1の関係を持つ必要があります。- 今回は
seed
で取り込んだデータをsource
として定義した後に、staging
レイヤのモデルを実行しています。このような構成とすることで、source
の段階で生データに対するデータ品質のチェックをできるメリットがあります。
- 今回は
ドキュメント
・intermediate層
│ ├── intermediates
│ │ └── marketing
│ │ ├── _int_marketing__models.yml
│ │ ├── int_dim_customers_joined_customers_and_addresses.sql
│ │ ├── int_dim_dates.sql
│ │ ├── int_dim_products.sql
│ │ └── int_fact_sales_joined_order_header_and_details.sql
サブディレクトリ
ステージングレイヤと同様にサブディレクトリで分けるが、ビジネス関連の領域ごとに分割することが推奨されています。
ここではマーケティング用途のデータをモデリングをしています。
ただし、はじめから複雑なディレクトリ構造を設計すればよいのではなく、常に複雑さをシンプルにすること、この目的に沿って構造化する必要があると言及されています。下記、ドキュメントより引用。
マート モデルが 10 個未満で、開発や使用に問題がない場合は、プロジェクトが成長して本当に必要になるまで、サブディレクトリを完全に省略してもかまいません (ステージングレイヤーは除きます。ステージングレイヤーでは、新しいソースシステムをプロジェクトに追加するたびにサブディレクトリを実装する必要があります)
命名規則
int_[entity]s_[verb]s.sql
厳密な名づけルールは困難だが、intermediateレイヤの動詞で考えるのが良い指針とされています。pivoted、joinedなどの変換処理を動詞化し命名します。
ドキュメントではint_payments_pivoted_to_orders.sql
と例示されています。
今回の例では、複数のテーブルを結合してモデル化しているため、ファイル名をjoined_A_and_B
としています。
このような命名規則によって、そのモデルがどのように構築されているのか、誰がみてもわかりやすくします。
a. 二重アンダースコアの削除
ビジネスに準拠した概念に移行することで、システムとエンティティを分離する必要がなくなるため、また、レイヤー内のエンティティと動詞の間には本質的な繋がりがあるため、二重アンダースコアの不要性が説明されています。
もしソースシステムレベルでの処理が必要な場合(後に結合するケースなど)は、二重アンダースコアを保持します。
そのほかの推奨事項
- materialized設定はephemerallyが推奨
- エンドユーザへは公開しない
- 構造の簡素化
- 中間モデルの目的はマートモデルの複雑さを軽減すること。
- マート層で多数のテーブルの結合を実装するのではなく、同様の目的を持つ複数のモデルを中間層で結合し、マート層ではより少ない数のテーブルからモデルを作成する。
- 中間モデルの目的はマートモデルの複雑さを軽減すること。
ドキュメント
・mart層
│ ├── marts
│ │ └── marketing
│ │ ├── _marketing__models.yml
│ │ └── obt_sales.sql
サブディレクトリ
マートが 10 個未満の場合、サブフォルダーはあまり必要ないようです。グループ化が必要な場合は、部門別にグルーピングする構造が推奨されています。
命名規則
明確なルールはなく、マートの粒度を形成する概念に基づいて、平易な英語を使用して命名するように言及されています。
そのほかの推奨事項
- materialized設定はtableまたはincremental
- materializedに関する一般的な設定方針
- 常に
view
から開始し(ストレージを占有せず、常に最新結果が得られるようにする) -
view
でクエリの実行が遅くなりすぎるときは、table
を選択 -
table
の構築が遅くなるときは、incremental
を設定する
- 常に
ドキュメント
Model contractsについて
ここからは、Model contractsについてです。
Model contractsとは、対象のモデルのデータ型や制約が定義通りであるか保証できる機能になります。
モデルの構築中に、列名、データ型、制約などのモデルの形状(shape)を明示的に定義することで、変換プロセスやそのモデルをクエリする際のリスク軽減につながります。
一連の保証を「契約」と定義されています。
・contractsのサポート
- SQL モデル
- モデルは次のいずれかとして実現されます
table
view
- ビューでは列名とデータ型のサポートは限定されていますが、constraintsはサポートされませんincremental
-on_schema_change:{append_new_columns,fail}
オプションのときサポートの対象
反対にサポートされないのは
- Python モデル
- materialized設定が
view
またはephemeral
- その他のリソースタイプ (
sources
、seeds
、snapshots
など)
・contractを定義する
models:
- name: <model_name>
# required
config:
contract: {enforced: true}
# model-level constraints
constraints:
- type: primary_key
columns: [first_column, second_column, ...]
- type: foreign_key # multi_column
columns: [first_column, second_column, ...]
to: ref('other_model_name')
to_columns: [other_model_first_column, other_model_second_columns, ...]
- type: check
columns: [first_column, second_column, ...]
expression: "first_column != second_column"
name: human_friendly_name
- type: ...
columns:
- name: first_column
data_type: string
# column-level constraints
constraints:
- type: not_null
- type: unique
- type: foreign_key
to: ref('other_model_name')
to_columns: [other_model_column]
- type: ...
constraints(制約)の定義
- type(必須):
not_null
,unique
,primary_key
,foreign_key
,check
,custom expression
のいずれか- name(オプション): この制約のわかりやすい名前。一部のデータ プラットフォームでサポートされています
columns(model-level Only): 制約を適用する列名のリスト
PostgreSQLで制約を定義する
PostgreSQLでの制約はversion13以降
がサポート対象のため注意が必要です。
その他のプラットフォームに依存した制限もあります。
・seedでデータ型を定義する
model contracts機能ではseeds
はサポート対象外ですが、seeds.yml
のconfig
オプションを活用することで、データ型の定義が可能です。
seeds:
- name: raw_order_headers
config:
column_types:
order_id: integer
customer_id: integer
order_date: date
total_amount: float
...
ただし、検証した限りでは、数値型(INT)のデータが入ってくるカラムに対して文字列型(VARCHAR)を指定してもチェックはスルーされます。(INTの入力に対しBOOLEANを指定するような明らかな差異はチェックしてくれます)
余談ですが、データコントラクトの文脈では、より上流からデータの品質を担保する重要性が説かれており、後続処理へ及ぼす影響を最低限にするためにも、seeds
やsource
層でもデータ品質をチェック機構は必要かもしれません。
最後に
dbtの公式ドキュメントに沿いながら過去のdbtプロジェクトを構造化してみました。
新たに知れたベストプラクティスは多く、学びが多かったです。
一部をつまみ食いして最適化を図るのではなく、ベストプラクティスに従い目的をもって全体の最適化を目指すことが、堅牢で柔軟性のある構造につながると感じました。
dbtベストプラクティスに照らしながら、プロダクトやデータ基盤、開発チーム、利用メンバーの成熟度に合わせてプロジェクトを更新必要性を感じました。
読んでくださった方に、何か一つでも得られるモノがある記事となりましたらうれしいです。