「データを3層に分ける考え方は分かった。では、dbtで実際にどう作ればよいのか」。設計の概念を理解したあと、いざ実装の段になって手が止まる方は少なくありません。層の意味づけや設計思想はメダリオンアーキテクチャ(Bronze/Silver/Gold)の解説に譲り、この記事ではdbtでの具体的な作り方に絞ってお伝えします。フォルダ構成からsources定義、命名規則、層ごとのmaterializationとテストまでを順に見ていきます。

dbtプロジェクトのフォルダ構成

まず`models`配下を層ごとのフォルダに分けます。Raw(生データ)はdbtの`source`として外部に定義し、Staging・(必要なら)Intermediate・Martをモデルとして作ります。役割と置き場所を最初に決めておくと、チームのどのメンバーが見ても迷いません。

フォルダ / 定義役割命名例
sources(YAML)Raw層。取り込み済みの生テーブルを参照対象として宣言source(‘app’, ‘orders’)
models/staging/1ソース1モデルで素直に整えるstg_orders.sql
models/intermediate/複数stagingをまたぐ再利用ロジック(任意)int_orders_enriched.sql
models/marts/(ドメイン別)用途別の最終テーブルmarts/finance/fct_daily_sales.sql

入口:sourcesでRaw層を宣言する

Raw層はdbtで作るのではなく、すでに取り込まれた生テーブルを`sources`として宣言します。あわせてfreshness(鮮度)を設定しておくと、取り込みが止まったときに気づけます。

# models/staging/_sources.yml
version: 2
sources:
  - name: app
    schema: raw
    tables:
      - name: orders
        loaded_at_field: _ingested_at
        freshness:
          warn_after: {count: 24, period: hour}
          error_after: {count: 48, period: hour}

Staging層:1ソース1モデルで素直に整える

Staging層は、ソース1テーブルにつき1モデルを基本にします。やることはリネーム・型変換・軽いフィルタまでで、JOINや集計、ビジネスロジックは持ち込みません。これは設計原則の「Silver層にロジックを持たせない」をdbtで具体化したものです。`source()`関数で必ず参照し、テーブル名を直書きしないのがポイントです。

-- models/staging/stg_orders.sql
with source as (
    select * from {{ source('app', 'orders') }}
),
renamed as (
    select
        id                            as order_id,
        customer_id,
        cast(amount as double)        as amount,
        cast(created_at as timestamp) as created_at
    from source
    where id is not null
)
select * from renamed

命名は`stg_<ソース>_<対象>`で統一します。Staging層はビューにしておくと、ストレージを使わず常に最新を返せます。

Intermediate層:再利用するロジックをまとめる(任意)

複数のstagingをまたぐJOINや、いくつものMartで使い回す共通計算がある場合だけ、Intermediate層(`int_`)を挟みます。最初から作る必要はありません。同じロジックを2か所以上で書きそうになったら切り出す、くらいの温度感がちょうどよいです。`ref()`で上流のstagingを参照します。

たとえば、注文と顧客を結合して各Martで使い回す「明細の土台」を作る例です。上流は必ず`ref()`で参照します。

-- models/intermediate/int_orders_enriched.sql
with orders as (
    select * from {{ ref('stg_orders') }}
),
customers as (
    select * from {{ ref('stg_customers') }}
)
select
    o.order_id,
    o.amount,
    o.created_at,
    c.customer_segment
from orders o
left join customers c
    on o.customer_id = c.customer_id

Mart層:用途別に組み立てる

Mart層は、ビジネスの質問に直接答えるテーブルをドメイン別フォルダに置きます。ファクトは`fct_`、ディメンションは`dim_`で揃えると、役割がひと目で分かります。上流は必ず`ref()`で参照し、リネージ(依存関係)が途切れないようにします。

-- models/marts/finance/fct_daily_sales.sql
select
    date(created_at) as sale_date,
    count(*)         as order_count,
    sum(amount)      as total_amount,
    avg(amount)      as avg_amount
from {{ ref('stg_orders') }}
group by 1

層ごとのmaterializationとテスト

層によって、テーブルの作り方(materialization)とかけるテストを変えます。`dbt_project.yml`でフォルダ単位にまとめて指定でき、個別モデルで上書きもできます。

materialization主なテスト
Stagingviewnot_null・unique(主キー)
Intermediateview / ephemeral上流で担保(最小限)
Marttable(大規模はincremental)relationships・accepted_values・ビジネスルール
# dbt_project.yml(層ごとの既定materialization)
models:
  my_project:
    staging:
      +materialized: view
    marts:
      +materialized: table

# models/staging/_stg_orders.yml(テスト)
models:
  - name: stg_orders
    columns:
      - name: order_id
        tests:
          - not_null
          - unique

つまずきやすい実装ポイント

設計が正しくても、実装の作法でつまずくとリネージや環境切り替えが壊れます。次の4点を最初にチーム規約として決めておくと安全です。

  • 必ず ref() / source() で参照する:テーブル名を直書きすると、リネージが途切れ、開発・本番の環境切り替えも効かなくなります。
  • Stagingにロジックを入れない:JOINや集計はIntermediate以降へ。Staging層の肥大化は保守性を一気に下げます。
  • 命名を統一する:stg_/int_/fct_/dim_ を徹底すると、どの層のモデルか名前だけで分かります。
  • テストなしでマージしない:CIで dbt build(実行+テスト)を回し、壊れたモデルが本番に出ないようにします。

まとめ

dbtで3層構造を実装する勘所は、Rawをsourcesで宣言し、Stagingは素直に整え、Martで用途別に組み立てること。そして層ごとにmaterializationとテストを変え、ref()/source()でリネージを保つことです。層を分ける考え方そのものはメダリオンアーキテクチャの解説にまとめていますので、概念と実装をあわせて押さえると、保守性の高い基盤になります。

よくある質問

Intermediate層は必ず作るべきですか?

いいえ。最初は不要です。複数のstagingをまたぐJOINや、いくつものMartで使い回す共通計算が出てきてから切り出せば十分です。同じロジックを2か所以上で書きそうになったタイミングが目安です。

Martはtableとincrementalのどちらにすべきですか?

まずはtableで十分です。データ量が増えてフル再計算の時間やコストが無視できなくなったら、incrementalに切り替えます。最初からincrementalにすると、冪等性や差分条件の管理が複雑になりがちです。

dbt以外のツールでも同じ構成にできますか?

できます。層を分ける考え方はツールに依存しません。SQLとオーケストレーションがあれば再現できます。設計思想はメダリオンアーキテクチャの解説を参照してください。