dbtプロジェクト構築に関する ベストプラクティス #1「概要」 #dbt

2023.09.19

アライアンス事業部 エンジニアグループ モダンデータスタック(MDS)チームのしんやです。

dbtはクラウド型データウェアハウス(DWH)におけるデータ変換に特化したツールです。非常に使い勝手が良く便利なツールである一方、様々な機能が提供されているのでいざ使ってみよう!となると『何をどうやって作り上げていけば良いんだろう?』『この場合のルールや制限はどういうものがあるの?どういう取り決めをもって扱えば良いんだろう?』という風に思うこともあるかと思います。(実際私自身そう感じました)

そんなユーザーの疑問や悩みを解決する、いわゆるdbtユーザー向けのガードレール的な存在となりうるコンテンツがdbt社から展開されています。それが『dbtベストプラクティスガイド(Best practice guides)』です。構造、スタイル、セットアップなど、dbt Labsの現在の視点を通した「ベストプラクティス」がまとめられています。

そこで当エントリでは、幾つか展開されている「dbtベストプラクティスガイド」で紹介されているコンテンツの中から『dbtプロジェクト構築』に関するものを読み解いていきたいと思います。まずは一番最初、『概要』の部分から。

目次

 

はじめに

dbtでは「アナリティクス・エンジニアリング」という言葉を掲げており、この語句を「その核心に於いて、人間の集団がより良い意思決定を大規模に、共同で行えるように支援すること」と意味付けています。私達が決定を下すための"帯域幅"(ここはリソースと言い換えても良さそう)は限られており、また協力的な社会的種族である我々(人間)は、他者との協力を最適化するためにシステムやパターンに依存しています。

数人が集って形成される共同プロジェクトではチームの限られた"意思決定の帯域幅"を、フォルダの場所やファイル名の命名・作成規約等ではなく、分析や関連するアクションに於いて発生するユニークで難しい問題に費やすことが出来るように、一貫して理解しやすいルールを確立しておくことが極めて重要であるとも言えます。

これらのことはdbtプロジェクトを構築する点に於いても言えます。優れたdbtプロジェクトを構築することは共同作業でもあり、各部門の専門知識を結集し、企業全体のゴールと物語をマッピングすることにあります。dbtプロジェクトにおいて多くの人々がそれぞれの専門知識を積極的に活用出来るようにするため、組織の規模拡大に直面してもアプローチ・維持しやすいプロジェクトであることを保つために、深く幅広いパターンセットを確立することが特に重要となります。

dbtは「ファイル」「フォルダ」「命名規則」「プログラミングパターン」で構成されています。どのようにグループ化するか、どのように分割するか、どのようにまとめるか、dbtプロジェクトでコード化されたデータ変換処理を整理するためにどのようなシステムを使うか。dbtではこういった「プロジェクトの構造」を考慮した形でデザインされています。

全てのdbtプロジェクトに於いて当てはまる基本原則の1つは「"ソース準拠"の状況から"ビジネス準拠"の状況へとデータを移動させる、"一貫した弧"を確立する必要性」です。 ソースとして変換されたデータは私達の制御の及ばない外部システムによって形成され、ビジネス変換されたデータは私達が定めたニーズや概念、定義によって形成されます。 プロジェクト内でどのようなパターンや規約を定義しようとも、このプロセスが変換レイヤーの基本的な目的であることには変わりはなく、dbtはそれらを実現するためのツールとして存在しています。

 

学習のゴール

このベストプラクティスを学んでいくことで期待できる効果(ゴール)は主に以下の内容となっています。

  • 典型的なdbtプロジェクトの構成方法に関する最新の推奨事項を網羅することが出来る
  • 各種推奨事項を包括的な例を以て説明することが出来る
  • それぞれの局面に於いて、dbt社がなぜそのようなアプローチを推奨しているのかを理解することで、いつどこでその推奨事項から逸脱し、組織独自のニーズにより適合させていくかを判断出来る

構成に関するベストプラクティスを学んでいくことで、dbtプロジェクトの構成要素がどのように組み合わされているのかをより深く理解でき、アナリティクスエンジニアの目的と原則が明確に、直感的に感じられるようになります。そして「構造化アプローチ」を意図的に取ることで、以下のような効果を得られます。

  • システムにおける「より狭いソースに適合したモデル」から私達が作成する「より広い、より豊かなビジネスに適合したデザインの狭いセット」へデータを移動させる上での「理想的な流れ」についての理解
  • 最適化されたモジュラーレイヤーに変換処理を積み重ねていくことで、それぞれの変換処理は1箇所だけに適用出来るということを理解
  • 構造を構成するファイル、フォルダ、マテリアライゼーションに規律あるアプローチを取ることで、データだけでなく、コードベースやそれがウェアハウスに生成する成果物を通じて、明確なストーリーを作成できることを理解

このベストプラクティスでは、以前投稿したdbtクイックスタートでも登場しているJaffle Shopのdbtプロジェクトを使って説明が為されていきます。なので予めdbtクイックスタートの内容を実践しておくと理解もしやすくなるでしょう。「Jaffle ShopはJaffle(閉じたトーストサンドイッチのオーストラリアでの呼び名、らしい)を売るレストランである」ことと「2つの主要なデータベースを持っている」ということをここでは知っておきましょう。

  • jaffle_shop:トランザクションデータベースのレプリカ
  • stripe:支払い処理に使用するStripeからの同期データ

 

dbtプロジェクト構造

下記の構成は、このベストプラクティスで取り組むことになるjaffle_shopのdbtプロジェクトの構造を一覧下したものです。この構造を踏まえて1つずつポイントを読み解いて行く形になります。強調行表示している箇所は後述で言及するポイントです。

jaffle_shop
├── README.md
├── analyses
├── seeds
│   └── employees.csv
├── dbt_project.yml
├── macros
│   └── cents_to_dollars.sql
├── models
│   ├── intermediate
│   │   └── finance
│   │       ├── _int_finance__models.yml
│   │       └── int_payments_pivoted_to_orders.sql
│   ├── marts
│   │   ├── finance
│   │   │   ├── _finance__models.yml
│   │   │   ├── orders.sql
│   │   │   └── payments.sql
│   │   └── marketing
│   │       ├── _marketing__models.yml
│   │       └── customers.sql
│   ├── staging
│   │   ├── jaffle_shop
│   │   │   ├── _jaffle_shop__docs.md
│   │   │   ├── _jaffle_shop__models.yml
│   │   │   ├── _jaffle_shop__sources.yml
│   │   │   ├── base
│   │   │   │   ├── base_jaffle_shop__customers.sql
│   │   │   │   └── base_jaffle_shop__deleted_customers.sql
│   │   │   ├── stg_jaffle_shop__customers.sql
│   │   │   └── stg_jaffle_shop__orders.sql
│   │   └── stripe
│   │       ├── _stripe__models.yml
│   │       ├── _stripe__sources.yml
│   │       └── stg_stripe__payments.sql
│   └── utilities
│       └── all_dates.sql
├── packages.yml
├── snapshots
└── tests
    └── assert_positive_value_for_total_amount.sql

ちなみにこちらは先日のdbtクイックスタート(dbt Core編) 実践後のプロジェクト構成です。幾つかファイルが存在しないフォルダなども散見されますね。

% brew install tree
% tree
.
├── README.md
├── analyses
├── dbt_packages
├── dbt_project.yml
├── logs
│   └── dbt.log
├── macros
├── models
│   ├── customers.sql
│   ├── schema.yml
│   └── staging
│       ├── stg_customers.sql
│       └── stg_orders.sql
├── seeds
├── snapshots
├── target
│   ├── catalog.json
│   ├── compiled
│   │   └── jaffle_shop
│   │       └── models
│   │           ├── customers.sql
│   │           ├── example
│   │           │   ├── customers.sql
│   │           │   ├── my_first_dbt_model.sql
│   │           │   └── my_second_dbt_model.sql
│   │           ├── schema.yml
│   │           │   ├── accepted_values_stg_orders_4f514bf94b77b7ea437830eec4421c58.sql
│   │           │   ├── accepted_values_stg_orders_a66eab964f1cf5f6babf515912d58fa9.sql
│   │           │   ├── not_null_customers_customer_id.sql
│   │           │   ├── not_null_stg_customers_customer_id.sql
│   │           │   ├── not_null_stg_orders_customer_id.sql
│   │           │   ├── not_null_stg_orders_order_id.sql
│   │           │   ├── relationships_stg_orders_96411fe0c89b49c3f4da955dfd358ba0.sql
│   │           │   ├── unique_customers_customer_id.sql
│   │           │   ├── unique_stg_customers_customer_id.sql
│   │           │   └── unique_stg_orders_order_id.sql
│   │           ├── staging
│   │           │   ├── stg_customers.sql
│   │           │   └── stg_orders.sql
│   │           ├── stg_customers.sql
│   │           └── stg_orders.sql
│   ├── graph.gpickle
│   ├── graph_summary.json
│   ├── index.html
│   ├── manifest.json
│   ├── partial_parse.msgpack
│   ├── run
│   │   └── jaffle_shop
│   │       └── models
│   │           ├── customers.sql
│   │           ├── example
│   │           │   ├── customers.sql
│   │           │   ├── my_first_dbt_model.sql
│   │           │   └── my_second_dbt_model.sql
│   │           ├── schema.yml
│   │           │   ├── accepted_values_stg_orders_4f514bf94b77b7ea437830eec4421c58.sql
│   │           │   ├── accepted_values_stg_orders_a66eab964f1cf5f6babf515912d58fa9.sql
│   │           │   ├── not_null_customers_customer_id.sql
│   │           │   ├── not_null_stg_customers_customer_id.sql
│   │           │   ├── not_null_stg_orders_customer_id.sql
│   │           │   ├── not_null_stg_orders_order_id.sql
│   │           │   ├── relationships_stg_orders_96411fe0c89b49c3f4da955dfd358ba0.sql
│   │           │   ├── unique_customers_customer_id.sql
│   │           │   ├── unique_stg_customers_customer_id.sql
│   │           │   └── unique_stg_orders_order_id.sql
│   │           ├── staging
│   │           │   ├── stg_customers.sql
│   │           │   └── stg_orders.sql
│   │           ├── stg_customers.sql
│   │           └── stg_orders.sql
│   ├── run_results.json
│   └── semantic_manifest.json
└── tests

データウェアハウスにおけるレイヤーに対応させたモデル構成

modelsディレクトリに存在する3つのフォルダ(及び配下のファイル)は、データ分析におけるデータの扱い方・用途にレイヤー分けを意味しています。このあたりのワードはデータ分析においては「データレイク」「データウェアハウス」「データマート」の文脈で"三層構造"として語られていたりしますし、データソース(データウェアハウス)におけるデータベース内の「テーブルの分け方」としても良く使われるものです。dbtでは対応するデータウェアハウス内の「テーブルの在り方」としてこういう定義をお勧めしますよ、と言っている訳ですね。

  • staging:ステージング層。ソースデータからatoms=初期モジュール構成要素を作成。テーブル生データ相当の定義を模したモデル(群)を格納する場所(で良いのかな?)
  • Intermediate:中間データ層。明確かつ具体的なロジックのレイヤーを積み重ねることで、私達が最終的に使いたいデータを準備するためのモデル群を格納する場所
  • Marts:マート層。中間データ層までで準備された情報を統合し、私達が実際に分析や各種アクションを行うために参照するデータを構成するモデル群を格納する場所

dbt固有の設定、構成

また、dbt固有の構成や仕組みがどのようにプロジェクトに関連しているかについても理解を深めることが出来ます。

  • dbtプロジェクトの全体的な構造を包括的に見直すことが出来る
  • YAML設定について詳しく説明することが出来る
  • dbtプロジェクトにおけるtestsseedsanalysisといったフォルダの使い方を理解出来る

 

まとめ

という訳で、dbtプロジェクト構築に関するベストプラクティス第1弾として概要パートの紹介でした。

データウェアハウスにおける「層」の説明が途中ありましたが、このあたりはデータウェアハウスを活用する時点である程度定めて(定まったうえで)運用するもの(運用されているもの)、という理解でしたので、dbtを使う前にこの辺はちゃんと考えておかないとね、というのを改めて実感しました。データウェアハウスと合わせてdbtを活用する場合は勿論この辺りの定義というか運用ルールを決めておく必要があるので、「dbtとして扱いやすいデータレイヤーの在り方、分け方を整えていく」という観点、切り口も今後の勉強で押さえておこうと思った次第でした。