🐈

dbt開発を効率よく行う11のプラクティスとプラスα

2024/07/18に公開

dbtを使った開発において、最適な使い方、効率の良いスマートな使い方ができているのか?と自問自答された開発者の方向けの記事です。
嬉しいことにdbt公式ドキュメントに開発を行う上でのベストプラクティスが公開してくれています!!
そのページでは、経験豊富なdbtユーザーの知恵を結集し、アナリティクス業務におけるdbtの最適な使い方が紹介されています。
これらのベストプラクティスを遵守することで、アナリティクスチームは可能な限り効果的に作業を進めることができます!
今の開発とベストプラクティスとを比較して、少しでも取り入れてみたいと思っていただけたら幸いです。
それでは行ってみよー!

dbt開発全般におけるプラクティス

1. 全てのdbtプロジェクトとはGit管理しましょう

dbtの開発は必ずGitでバージョン管理を行いましょう。
新機能の開発や、バグの修正などの全てのコード変更はバージョン管理を行い、適宜コメントを残し、振り返ることが出来るようにしましょう。
そして、mainブランチにマージする前に、コードレビューを行うことをお勧めします。
コードレビューについては、Recceなどのパッケージ機能を用いるとレビューの精度が上がります。

2. 開発環境と本番環境を分けて開発しましょう

dbt開発において、開発と本番さらには検証などといった環境を分けて管理しましょう。
dbtにはprofiles.yml内にてターゲットを使用することで、各環境事への使い分けが簡単にできます。
コマンドラインからdbtを実行する場合はdevターゲットを使用し、本番環境から実行する場合はprodターゲットに対してのみ実行することをお勧めします。

3. dbtスタイルガイドを活用しましょう

SQLのスタイル、フィールドの命名規則、その他dbtプロジェクトのルールは、特に複数のdbtユーザーがコードを書いているプロジェクトでは、明文化しておくことをお勧めします。
スタイルガイドについては、こちらを参照ください。
https://zenn.dev/kyami/articles/049232a17db17d

4. ref関数を使いましょう

dbtにモデル開発において、テーブルを参照する際はテーブル名のフルパスを指定するのではなく、ref関数を利用しましょう。
例:
 select * from PROD_DB.PUBLIC.CUSTOMERS ❌
 select * from {{ ref('CUSTOMERS') }} ⭕️

ref関数を使うことで、dbtは依存関係を推測し、モデルが正しい順序でビルドされることを保証します。
また、現在のモデルが作業している環境と同じ上流のテーブルやビューから選択されるようにしましょう。
下流から上流の参照といった逆行が起きないよう注意しましょう。

5. 生データはsourceとして定義しましょう

dbtプロジェクトは、データベースに保存されている生データに依存します。
例えば、ソースデータをAPIやログ収集ツールを使って取得蓄積している場合、APIやツールの仕様の変更により、データ型、名前やカラムの増減などが起こり得ます。
このような場合、生データが一か所で参照されるだけであれば、モデルの更新が容易になります。
さらには、sourceとして定義することで、次のようメリットがあります。

  • リネージ図にソースデータを描画が可能
  • ソースデータに対するdbt testの実施が可能
  • freshnessを使いソースデータの鮮度をチェック出来る
  • ソースデータのドキュメント化が可能

6. カラム名やデータ型は上流レイヤーで変換しましょう

カラム名やデータ型は最上流モデル(ソースで取り込んだ次のレイヤー)で変換しましょう。
生データは通常、ソースに準拠した構造、つまりソースが定義するスキーマと命名規則に従って格納されています。
そのため、データモデリングの要件に沿った名前や単位、データ型の変換が必要となります。
例えば、上流のレイヤーにて、次のような変換を行うことがある。

  • 1つのソースからのみ選択するプロジェクト内で使用する規則に合わせてフィールドとテーブルの名前を変更する
    (たとえば、すべての タイムスタンプの名前が <event>_at になるようにする)。
  • 日付をUTCに、価格をドルに変更する

ただし、これらの規約は、プロジェクトのコーディング規約やスタイルガイドなどで宣言する必要があります。
上流レイヤーでの変換を行うことで、以降のすべてのデータモデルは、これらのモデルの上に構築する必要があり、重複するコードを減らすことができるメリットがあります。

7. 複雑なモデルはより小さく分割しましょう

複雑なモデルには、複数の共通テーブル式(CTE)が含まれることがよくあります。

CTE例
with rename_columns as (

    select

        id as customer_id,
        lower(first_name) as customer_first_name,
        lower(last_name) as customer_last_initial

    from {{ ref('raw_customers') }}
)

select * from rename_columns

dbtでは、このようなCTEを別々のモデルに分割して、その上に構築することができます。
そうすることで次のメリットが生まれます。

  • CTEを別のモデルに分割することで、いくつもの下流モデルからそのモデルを参照できるようになり、コードの重複を減らすことができます
  • CTEを別のモデルに分割することで、より大きなモデルから独立してこの変換をテストすることができます
  • CTEを個別のモデルに分割することで、他のdbtユーザー(または将来の自分)がコードを見る際の認知的負荷を軽減することができます

8. モデルをグループ化できるならディレクトリ構造として表現しましょう

dbt_project.ymlファイルで設定を指定することで、モデルのグループを設定することができます。
例えば、企業の部署毎にモデルを作成するといったケースの場合、営業、総務、開発、運用などをディレクトリ毎に切って構造化することを推奨します。
さらに、ディレクトリ内の入れ子構造を利用することで、以下のことが容易になります。

  • dbt_project.ymlファイルで設定を指定することで、モデルのグループを設定可能
  • モデル選択構文を使用することで、DAGのサブセクションを実行可能
  • モデリングステップを共同作業者に伝える。
  • モデルの許容される上流の依存関係に関する規約(ガイドライン)を作成可能
    例えば、「martsディレクトリ内のモデルは、martsディレクトリ内の他のモデル、またはstagingディレクトリ内のモデルからのみ選択できる」など

9. 必ずテストは実施しましょう

dbtは、モデルによって生成された結果に対してテストするフレームワークを提供します。
生成したモデルに対して、テストを実施することで、SQLが期待通りの方法でデータを変換していることと、ソースデータに期待通りの値が含まれていることの両方を保証することができます。

10. 積極的にカスタムスキーマを使用しましょう

メイン開発者以外のメンバー(例えば、新規参入者やデータ利活用を目的とした他部署のメンバー)がデータウェアハウスにアクセスした際にデータ構造の理解を促進させるためもカスタムスキーマを使用しましょう。
カスタムスキーマを利用することで、スキーマ名やテーブル名に接頭字(raw_ , stg_ , fct_ , dim_ , mart_)を付けることが出来ます。

11. プロジェクトに則したマテリアライゼーションを選択しましょう

dbtはテーブルやView、Incrementalなど、いくつかのマテリアライゼーションをサポートしています。
それらの特性を理解して、適切に構築を行いましょう。
以下にマテリアライゼーションのベストプラクティスが紹介されているので、そちらも参考にしてみましょう。
https://docs.getdbt.com/best-practices/materializations/1-guide-overview

開発におけるTIPS

ここからは開発におけるちょっとしたTIPSの紹介です。

1. モデル選択構文を使って開発を行う

モデル開発時、開発したモデルとその影響下にあるモデルだけをコンパイル、リリース、テストすることが理にかなっているます。
そのため、モデル選択構文(model selection syntax)を利用することをお勧めします。

2. 作成したモデルのリリース前に

コードの変更を安全にマージするためには、その変更がプロジェクトの別の場所で破損を引き起こさないことを確認する必要があります。
そのため、本番データから切り離されたサンドボックス環境でモデルとテストを実行することをお勧めします。
一般的にプルリクエストを作成後、CIフローとして自動でサンドボックス環境へのモデル実行が推奨されています。

同時に、プロジェクト内のすべてのモデルを実行し、テストするには時間(とお金)がかかります。
dbtは、以前の本番環境からの成果物と比較することで、どのモデルが変更されたかを判断し、変更されていない親モデルの上にそれらをビルドすることができます。
次のコマンドでartifactsファイルから変更のあったモデルとそれに関連するモデルを判断し、runとtestを実行することが可能です。

dbt run -s state:modified+ --defer --state path/to/prod/artifacts
dbt test -s state:modified+ --defer --state path/to/prod/artifacts

dbtプロジェクトにおけるTIPS

3. 開発時に処理されるデータを制限する

開発環境では、実行時間を速くすることで、より素早くコードを反復することができます。
ターゲット名に基づいてデータを制限するパターンを使用することで、頻繁に実行を高速化しています。
以下のSQLの下から3行の部分でターゲットがdevつまり開発環境であれば、現在時刻から3日前のデータを対象にすることで、データ量を減らし、実行時間を短縮することができます。

select
*
from event_tracking.events
{% if target.name == 'dev' %}
where created_at >= dateadd('day', -3, current_date)
{% endif %}

4. post-hookを使ってテーブルの権限設定を行う

dbtによって作成されたオブジェクトにパーミッションが適用されるようにするには、フックからグラントステートメントを使用します。
このグラント文をフックにコード化することで、これらのパーミッションをバージョン管理し、繰り返し適用することができます。
例えば、次の設定をモデルSQLファイルに記述することで、bi_userはこのモデルのselect権限を付与します。

{{ config(materialized = 'table', grants = {
    'select': 'bi_user'
}) }}

5. ソース中心の変換とビジネス中心の変換を分ける

1つのモデルのSQLファイル内でソースデータに対する変換とビジネスロジックの変換を一緒にしないことで
す。
ソースデータの変換とは、例えば、列の再エイリアスや再キャスト、ソースデータの結合、結合、重複排除などです。
ビジネスロジックの変換とは、ビジネスに関連するエンティティやプロセスを表すモデルにデータを変換したり、SQLでビジネス定義を実装したりするビジネス中心の変換です。
ソース中心のロジックとビジネス中心のロジックの区別を明確にするために、この2つのタイプの変換を別のモデルに分けることが最も便利です。

6. Jinjaが生成する空白を管理する

マクロやJinjaの他の部分をモデルで使用している場合、コンパイルされたSQL(target/compiledディレクトリにあります)には不要な空白が含まれることがあります。
生成される空白を制御する方法については、Jinjaのドキュメントをチェックしてください。

最後に

上記記載のいくつかのプラクティスは、dbtの

にも似た内容が記載されていますのでご参考ください。

Discussion