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

この記事で身につくこと
- dbtプロジェクトのフォルダ構成と、各層に置くファイルの見分け方
- Staging・Intermediate・Martそれぞれの「やること/やらないこと」
- 層ごとのmaterializationとテストの選び方、初回セットアップの1週間ロードマップ
3層構造の早見表
まず全体像を1枚の表で押さえます。各章の詳細を読む前に、層ごとの役割・粒度・materialization・主なテストを俯瞰しておくと迷子になりません。
| 層 | 目的 | 粒度 | 命名 | materialization | 主なテスト |
|---|---|---|---|---|---|
| Raw / sources | ELで取り込んだ生テーブルの宣言(作らない) | ソースのまま | YAMLで source('app','orders') | — | freshness |
| Staging | リネーム・型変換・軽いフィルタ | 1ソース1モデル | stg_<source>_<table> | view | unique / not_null / relationships |
| Intermediate(任意) | 複数stagingをまたぐ再利用ロジック | 粗粒度の中間 | int_<topic>_<action> | ephemeral / table | row_count / expected_values |
| Mart | ビジネスの質問に直接答える | 用途別の集計・結合 | fct_* / dim_* | table or incremental | accepted_values / dbt-utils各種 |
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層はビューにしておくと、ストレージを使わず常に最新を返せます。
Staging層でよく迷うのが「どこまで手を入れていいか」です。線引きを判定表にまとめておくと、レビューで揉めません。
| Stagingで OK | Staging では NG |
|---|---|
カラムのリネーム(id → order_id) | 他テーブルとのJOIN |
型変換(cast(amount as double)) | GROUP BY による集計 |
| NULL や不正値の除外 | ビジネスロジック(税率計算・区分の判定) |
| タイムゾーン統一・タイムスタンプ整形 | ウィンドウ関数による履歴処理 |
| 不要カラムの落とし込み | マスタとの突き合わせによる補完 |
Intermediate層:再利用するロジックをまとめる(任意)
複数のstagingをまたぐJOINや、いくつものMartで使い回す共通計算がある場合だけ、Intermediate層(`int_`)を挟みます。最初から作る必要はありません。同じロジックを2か所以上で書きそうになったら切り出す、くらいの温度感がちょうどよいです。`ref()`で上流のstagingを参照します。
たとえば、注文と顧客を結合して各Martで使い回す「明細の土台」を作る例です。上流は必ず`ref()`で参照します。
Intermediate層を作るか作らないかは、判定基準を先に決めます。「なんとなく作る」と Mart 側の見通しが逆に悪くなります。
| 作る | 作らない |
|---|---|
| 同じJOINを2つ以上のMartで書きそう | 1つのMartでしか使わない結合 |
| 複雑な履歴処理(SCD Type 2など)を共通化したい | 単純なリネームや型変換だけ |
| 粒度変換(明細→日次サマリ)を複数下流で使う | 粒度変換が下流で1回だけ必要 |
SCD Type 2 の履歴処理を int 層に切り出したい場合は、dbt snapshotsでSCD Type 2を実装するで snapshot と int の分担を整理しています。
-- 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()`で参照し、リネージ(依存関係)が途切れないようにします。
Mart 層は命名とフォルダ配置で伸縮しやすさが決まります。ドメイン別に切り、fct(事実)と dim(属性)で接頭辞をそろえます。
| ドメイン | ファクト例(fct_) | ディメンション例(dim_) |
|---|---|---|
| finance | fct_daily_sales, fct_invoices | dim_product, dim_customer |
| marketing | fct_campaign_touchpoints | dim_channel, dim_utm |
| product | fct_sessions, fct_events | dim_user, dim_feature |
データ量が増えて Mart のフル再計算コストが気になり始めたら、dbt incremental models の設計と落とし穴で差分更新の切り替え条件を整理しています。
-- 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`でフォルダ単位にまとめて指定でき、個別モデルで上書きもできます。

層ごとの標準は次のとおりです。まずこのデフォルトから入って、必要が出てから個別に上書きします。
| 層 | デフォルト | 理由 | 上書きするとき |
|---|---|---|---|
| Staging | view | ストレージを使わず常に最新、再計算が軽い | 大量スキャンで下流が重くなる場合はtableへ |
| Intermediate | ephemeral or table | 下流に1回しか使わないならephemeral、複数で使うならtable | 実行時間が長い→incremental化 |
| Mart | table | BIから直接叩かれる速度が最優先 | データ量とコストが増えたらincrementalに切り替え |
| 層 | 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

テストは層ごとに責務を分けます。Stagingは「ソースの契約」を守る、Martは「ビジネスの意味」を守る、と役割が違います。
| 層 | 入れるテスト | 目的 |
|---|---|---|
| Staging | unique, not_null, relationships | ソースからの取り込みが壊れていないか |
| Intermediate | row_count 差分、期待値との整合 | 結合や粒度変換で欠落や重複が出ていないか |
| Mart | accepted_values, dbt_utils.expression_is_true, カスタムテスト | ビジネスルールが守られているか(マイナス金額なし、日次合計 = 明細合計 など) |
テストの粒度と CI 統合の実践はdbt tests 実践:generic・singular・dbt-expectations の使い分けを、CI で dbt-checkpoint を組み合わせて自動強制する運用はdbt-checkpoint とはで扱っています。
つまずきやすい実装ポイント
設計が正しくても、実装の作法でつまずくとリネージや環境切り替えが壊れます。次の4点を最初にチーム規約として決めておくと安全です。
- 必ず ref() / source() で参照する:テーブル名を直書きすると、リネージが途切れ、開発・本番の環境切り替えも効かなくなります。
- Stagingにロジックを入れない:JOINや集計はIntermediate以降へ。Staging層の肥大化は保守性を一気に下げます。
- 命名を統一する:stg_/int_/fct_/dim_ を徹底すると、どの層のモデルか名前だけで分かります。
- テストなしでマージしない:CIで dbt build(実行+テスト)を回し、壊れたモデルが本番に出ないようにします。
初回セットアップの1週間チェックリスト
ゼロから3層構造を立ち上げるとき、何から手をつけるか迷うことが多いです。実務で回している標準的な1週間の進め方を並べます。
| Day | やること | ゴール |
|---|---|---|
| Day 1 | dbt プロジェクト初期化、Snowflake/BigQuery/Databricks の接続、リポジトリ作成 | dbt debug が通る |
| Day 2 | models/staging/_sources.yml で対象ソースを宣言、freshness を設定 | dbt source freshness が緑 |
| Day 3 | 主要 3〜5 テーブルの stg_ を作成、命名規則をチームで合意 | dbt build –select staging が通る |
| Day 4 | 最初の Mart を1つ作る(例:fct_daily_sales)、materialization は table | BIから叩ける状態 |
| Day 5 | Staging に unique/not_null、Mart に accepted_values を最低1つずつ | dbt build 全緑 |
| Day 6 | CI(GitHub Actions等)で dbt build を回す、PR で通らないとマージできない設定 | PR で dbt build 自動実行 |
| Day 7 | docs 生成と公開、ドキュメント更新の運用ルール決定 | dbt docs generate → 社内公開 |
ドキュメント運用の負担を下げたいときは、dbt-osmosis で description の自動継承を回すと、YAML 保守が現実的なコストに収まります。CIで壊れたモデルを止める仕組みは dbt-checkpoint が定番です。
まとめ
dbtで3層構造を実装する勘所は、Rawをsourcesで宣言し、Stagingは素直に整え、Martで用途別に組み立てること。そして層ごとにmaterializationとテストを変え、ref()/source()でリネージを保つことです。層を分ける考え方そのものはメダリオンアーキテクチャの解説にまとめていますので、概念と実装をあわせて押さえると、保守性の高い基盤になります。
実際の環境に3層構造を導入するとき、既存の SQL 資産の移行、CIの組み方、Snowflake / BigQuery / Databricks 固有の設定で詰まりやすい箇所があります。DE-STK の初回相談(30分・無料)では、あなたのチームの現状を聞きながら、フォルダ構成の初期案とテスト戦略のたたき台まで一緒に描けます。
あわせて読みたい dbt 関連記事
3層構造の土台ができたら、周辺トピックに広げていきます。テストと運用、コスト、モデリングの深掘り、代替ツールの視点で整理しています。
- メダリオンアーキテクチャとは?Bronze/Silver/Gold の設計パターン徹底解説 — 概念面の再確認
- dbt incremental models の設計と落とし穴:full-refresh が回らない時の処方箋
- dbt tests 実践:generic・singular・dbt-expectations の使い分け
- dbt + Snowflake のコスト最適化:月末に跳ねるクレジットを止める
- dbt snapshots で SCD Type 2 を実装する:顧客や商品の履歴を残す定石
- dbt-osmosis とは:description を自動継承してドキュメント運用を楽にする
- dbt-checkpoint とは:CI で dbt プロジェクトの品質を自動強制する
- SQLMesh vs dbt:新興データ変換ツールの強みと、移行の現実
- Data Vault 2.0 とは:Hub・Link・Satellite で作る、変化に強いデータモデリング
- dbt Semantic Layer とは:MetricFlow とメトリクス定義の実装ガイド
よくある質問
Intermediate層は必ず作るべきですか?
いいえ。最初は不要です。複数のstagingをまたぐJOINや、いくつものMartで使い回す共通計算が出てきてから切り出せば十分です。同じロジックを2か所以上で書きそうになったタイミングが目安です。
Martはtableとincrementalのどちらにすべきですか?
まずはtableで十分です。データ量が増えてフル再計算の時間やコストが無視できなくなったら、incrementalに切り替えます。最初からincrementalにすると、冪等性や差分条件の管理が複雑になりがちです。
dbt以外のツールでも同じ構成にできますか?
できます。層を分ける考え方はツールに依存しません。SQLとオーケストレーションがあれば再現できます。設計思想はメダリオンアーキテクチャの解説を参照してください。
Staging をビューにするとクエリが遅くなりませんか?
ソースが素直なテーブルであれば、モダンDWH(Snowflake / BigQuery / Databricks)ではビューでも遅くなりません。逆に、Staging を table にすると常に最新でなくなり、ストレージ課金も乗ります。ビューで詰まってから table 化を検討する順で問題ありません。
1ソース1モデルの原則から外していい例はありますか?
あります。同じソースが複数の粒度で使われるとき(イベントログを「セッション単位」と「日次サマリ単位」で使い分けるなど)は、Staging を目的別に stg_events_session, stg_events_daily と分けます。逆に、単純な差別化がないのに1テーブルから2つ以上の stg を作るのは避けます。
Mart のテーブル数が増えて管理しにくくなってきました。どう整理しますか?
ドメイン別フォルダの下にさらにサブフォルダを切ります(例:marts/finance/monthly/)。命名の接頭辞(fct_, dim_)と、更新頻度(daily/hourly/monthly)を組み合わせておくと、後から見て何のためのテーブルか一目で分かります。数が本当に多くなったら、セマンティックレイヤーとメトリクス管理を挟んでメトリクス定義側に責務を移す選択肢もあります。
データエンジニア入門:取り込みから配膳まで
関連記事を順序立てて読みながら、ステップごとに4択クイズで理解を確認できる学習パスです。登録不要・進捗自動保存。
学習パスを始める →