「データを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 | 主なテスト |
|---|---|---|
| Staging | view | not_null・unique(主キー) |
| Intermediate | view / ephemeral | 上流で担保(最小限) |
| Mart | table(大規模は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とオーケストレーションがあれば再現できます。設計思想はメダリオンアーキテクチャの解説を参照してください。