dbt 1.9→2.0移行で半年ハマった話|本番で気づいたモデル設計の現実
「メジャーバージョンアップくらい余裕」と思ってたら想像の3倍しんどかった。run_results.jsonのスキーマ変更やStateフラグの罠など、本番でやらかした失敗を正直に書きます。
dbt 1.9→2.0移行で半年ハマった話|本番運用で気づいたモデル設計とテスト戦略の現実
去年の秋から今年の春にかけて、うちのチームで dbt を 1.9 系から 2.0 系に移行した。「メジャーバージョンアップだから多少大変だろうな」とは思ってたけど、想像の3倍くらいしんどかった。同時に、移行を通じて「あ、自分たちのモデル設計が根本的にまずかったんだ」という気づきもあって、結果的にはやってよかったと思ってる。
教科書的な変更点の羅列じゃなくて、実際に本番環境で半年運用して気づいた設計の勘どころや、テスト戦略でやらかした話を正直に書く。ちょうど最近「データ品質管理のベストプラクティス」を調べてた方には データ品質管理2026年版:最新ツール&ベストプラクティス も合わせて読んでほしい。
dbt 2.0 で何が変わったのか、実際に移行して痛感したこと
dbt 2.0 は 2025年末にリリースされて、2026年の現時点では 2.1 系が安定版になってる。主要な変更点は以下の通りで、これが移行でハマった原因でもある。
| カテゴリ | 変更内容 | 影響度 |
|---|---|---|
| Python要件 | Python 3.8 サポート終了、3.10+ 必須 | 中 |
dbt_project.yml | config-version: 2 廃止、構造変更あり | 高 |
| Deprecation | run_results.json のスキーマ変更 | 高 |
| Unit Testing | dbt test → dbt test --select unit_tests で分離 | 中 |
| Semantic Layer | MetricFlow の統合強化、YAML定義が刷新 | 高 |
| Python models | dbt-spark / dbt-databricks での挙動変更 | 環境依存 |
| State management | --state フラグの挙動が微妙に変わった | 高 |
特にしんどかったのが run_results.json のスキーマ変更。うちは CI/CD の中でこの JSON を Python スクリプトでパースしてエラー通知を飛ばしてたんだけど、フィールド名が変わってて本番で気づくというやらかしをやった。Staging 環境で CI はパスしてたのに本番で通知が飛ばなくなるという、地味に最悪なパターン。静かに壊れてる系のバグって本当に発見が遅れるんだよな。
# dbt_project.yml の変更例
# 旧 (1.x系)
config-version: 2
name: 'my_project'
version: '1.0.0'
# 新 (2.0系) - config-version は不要になった
name: 'my_project'
version: '1.0.0'
# require-dbt-version: '>=2.0.0' # 追加推奨
# run_results.json のパース、v2.0で変わったフィールド
import json
# 旧スキーマ
# result['timing'][0]['completed_at'] # これが変わった
# 新スキーマ
with open('target/run_results.json') as f:
run_results = json.load(f)
for result in run_results['results']:
# v2.0 から execution_time は直接フィールドに
print(f"{result['unique_id']}: {result.get('execution_time', 'N/A')}s")
# status フィールドは 'success' / 'error' / 'warn' で変わらず
print(f"Status: {result['status']}")
ちゃんとテストしてなかった自分たちが悪いのは分かってる。ただ、変更ログに「run_results.json のスキーマ変更あり」とだけ書いてあって詳細が追いにくかったのも正直つらかった。リリースノートは本当にちゃんと読もうな、と改めて思った。
モデル設計の失敗パターンと、半年かけて辿り着いた構成
うちのチームが最初にハマったのが、Staging / Intermediate / Mart 層の分け方だった。「なんとなく staging は生データのクリーニング、mart はビジネスロジック」という理解で進めてたら、Intermediate 層がカオスになってきた。
flowchart TB
subgraph Sources["Sources (RAWレイヤー)"]
S1[Salesforce]
S2[BigQuery Export]
S3[Stripe API]
end
subgraph Staging["Staging Layer (stg_)"]
ST1[stg_salesforce__accounts]
ST2[stg_bigquery__events]
ST3[stg_stripe__payments]
end
subgraph Intermediate["Intermediate Layer (int_)"]
I1[int_accounts__enriched]
I2[int_payments__attributed]
I3[int_events__sessionized]
end
subgraph Mart["Mart Layer (mart_)"]
M1[mart_finance__revenue]
M2[mart_sales__pipeline]
M3[mart_product__funnel]
end
S1 --> ST1
S2 --> ST2
S3 --> ST3
ST1 --> I1
ST3 --> I2
ST2 --> I3
I1 --> M2
I2 --> M1
I1 --> M1
I3 --> M3
最初は Intermediate 層を作らずに staging から mart に直結してた。それが半年で90本以上のモデルになったとき、「このロジック、3つの mart モデルに同じ SQL が重複してる」という状況に陥った。重複コードの修正漏れで数字がずれるという事故が2回起きてから、ようやく Intermediate 層を真剣に設計し直した。正直、もっと早くやるべきだったと思う。
今のチームのルールはこうなってる。
| 層 | 命名 | 役割 | ビジネスロジック |
|---|---|---|---|
| Staging | stg_ | ソース1つに対して1モデル。型変換・カラム名の標準化のみ | ❌ 入れない |
| Intermediate | int_ | 複数ソースの結合、ビジネスロジックの分解 | ✅ ここが主戦場 |
| Mart | mart_ | BIツール・ダウンストリームが直接参照。集計・絞り込みのみ | △ 最小限 |
個人的には int_ の設計が一番重要だと思ってる。ここを雑にすると全部が雑になる。命名は int_{ドメイン}__{動詞+名詞} で統一していて、int_payments__attributed みたいな感じ。ファイル名だけで「このモデルが何をしているか」が分かるのが地味に快適。
dbt テスト戦略、正直やりすぎた話と2026年の落とし所
テストは最初張り切りすぎた。全カラムに not_null と unique を書いて、さらに dbt-expectations で型チェックも入れたら、テスト実行時間が 40分を超えてしまって CI/CD のボトルネックになった。
xychart-beta
title "テスト最適化前後の実行時間比較"
x-axis ["最適化前", "unit testのみ", "選択的テスト", "最終構成"]
y-axis "実行時間(分)" 0 --> 50
bar [42, 8, 18, 12]
40分→12分はかなり大きい。バランスを取るために、今は層ごとにテスト戦略を分けて管理してる。
# models/staging/salesforce/stg_salesforce__accounts.yml
version: 2
models:
- name: stg_salesforce__accounts
description: "Salesforce Account の標準化ビュー"
config:
contract:
enforced: false # stagingはcontractなし
columns:
- name: account_id
description: "Salesforce Account ID"
data_tests:
- not_null
- unique
- name: account_name
data_tests:
- not_null
- name: created_at
data_tests:
- not_null
# models/mart/finance/mart_finance__revenue.yml
version: 2
models:
- name: mart_finance__revenue
description: "月次売上サマリー。Finance チームが直接使用"
config:
contract:
enforced: true # martはcontractを強制
constraints:
- type: not_null
columns: [month, total_revenue_jpy]
columns:
- name: month
data_type: date
data_tests:
- not_null
- unique
- name: total_revenue_jpy
data_type: bigint
data_tests:
- not_null
- dbt_utils.accepted_range:
min_value: 0
dbt 2.0 から model contracts が安定版になってて、これは地味に便利。特に mart 層で BI ツールと API が直接依存してるモデルに対して、スキーマ変更を明示的に強制できるのがいい。「誰かが気づかずにカラム名変えた」という事故がゼロになった。mart 層だけでもまず入れることをおすすめしたい。
dbt 2.0 で導入された Unit Testing の使い方:
dbt 1.8 で Preview になってた Unit Testing が 2.0 で GA になった。これはマジで助かってる。
# tests/unit/test_int_payments__attributed.yml
unit_tests:
- name: test_revenue_attribution_logic
model: int_payments__attributed
given:
- input: ref('stg_stripe__payments')
rows:
- {payment_id: 'pay_001', amount_jpy: 10000, status: 'succeeded', created_at: '2026-01-15'}
- {payment_id: 'pay_002', amount_jpy: 5000, status: 'failed', created_at: '2026-01-15'}
- {payment_id: 'pay_003', amount_jpy: 8000, status: 'succeeded', created_at: '2026-01-16'}
expect:
rows:
- {payment_id: 'pay_001', is_successful: true, amount_jpy: 10000}
- {payment_id: 'pay_003', is_successful: true, amount_jpy: 8000}
# pay_002 は failed なので除外される
データを実際に流さずにロジックをテストできるのが便利で、CI の初期段階で Unit Testing だけ先に回すようにした。実際のデータに依存しないから爆速で終わる。うちの環境だと 100 テスト以上あっても 3 分以内に終わる。
CI/CD パイプラインの設計、Slim CI で何を変えたか
dbt の CI/CD で「毎回全モデルを実行する」というやり方を続けてたら、PR マージのたびに 30 分以上かかるようになってきた。dbt Cloud を使ってるチームは Slim CI が最初から使いやすい設定になってるけど、うちは dbt Core + GitHub Actions 構成だったのでちょっと工夫が必要だった。
flowchart LR
PR[PR作成] --> A[Unit Tests]
A --> B{pass?}
B -->|No| FAIL1[❌ CI失敗]
B -->|Yes| C[Changed Models Detection]
C --> D[Slim CI実行]
D --> E[Data Tests on Changed]
E --> F{pass?}
F -->|No| FAIL2[❌ CI失敗]
F -->|Yes| G[✅ マージ可能]
MERGE[マージ] --> H[Full Run - Prod]
H --> I[All Data Tests]
I --> J[Alerting]
# .github/workflows/dbt_ci.yml (抜粋)
name: dbt CI
on:
pull_request:
branches: [main]
jobs:
dbt-unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dbt
run: pip install dbt-bigquery==2.1.0
- name: Run Unit Tests Only
run: |
dbt deps
dbt test --select unit_tests
env:
DBT_PROFILES_DIR: ./
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}
dbt-slim-ci:
needs: dbt-unit-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 差分検出に必要
- name: Download production manifest
run: |
# 本番の manifest.json を取得
gsutil cp gs://my-dbt-artifacts/manifest.json target/prod_manifest.json
- name: Slim CI - Changed models only
run: |
dbt build \
--select state:modified+ \
--defer \
--state target/ \
--exclude tag:large_full_scan
これで PR ごとの CI 実行時間が平均 38分 → 11分に縮まった。state:modified+ で変更されたモデルとその downstream だけを対象にするのがポイント。
ただ、--defer の挙動が dbt 2.0 で微妙に変わってて、--state に渡す manifest.json が 2.0 形式である必要がある。これも移行時にハマった。旧バージョンの manifest.json を渡すと無言でスキップされるモデルが出てくるんだけど、エラーにならないから気づくのが遅れる。皆さんはどうしてます? Slack で聞いたら「manifest.json のバージョンチェックを CI に入れた」という対策を教えてもらって、うちもそれで対処した。
Semantic Layer と MetricFlow、チームで使い始めてわかった現実
2026年時点で dbt の Semantic Layer(MetricFlow)はだいぶ成熟してきた。去年まで「本番で使うには早い」と思ってたけど、dbt 2.0 での統合強化を受けて今年からチームに導入してみた。
# models/semantic_models/sem_revenue.yml
semantic_models:
- name: revenue
description: "売上に関するセマンティックモデル"
model: ref('mart_finance__revenue')
entities:
- name: month
type: primary
expr: month
dimensions:
- name: revenue_month
type: time
expr: month
type_params:
time_granularity: month
- name: product_category
type: categorical
measures:
- name: total_revenue
description: "月次総売上 (JPY)"
agg: sum
expr: total_revenue_jpy
- name: avg_order_value
description: "平均注文金額"
agg: average
expr: order_amount_jpy
metrics:
- name: monthly_revenue
description: "月次売上合計"
type: simple
type_params:
measure: total_revenue
- name: revenue_growth_mom
description: "前月比売上成長率"
type: derived
type_params:
expr: (monthly_revenue - lag(monthly_revenue, 1)) / lag(monthly_revenue, 1)
metrics:
- name: monthly_revenue
これで Looker や Metabase から Semantic Layer を経由してクエリすると、メトリクスの定義がコードとして管理される。「売上の定義が BI ツールごとにバラバラ」という問題を根本的に解消できるのはかなり大きい。
ただ、正直まだ全社展開はできてなくて、Finance と Sales チームの主要メトリクス 20 個だけを Semantic Layer 管理に移行した段階。全モデルに適用するのはもう少し先になりそう。MetricFlow の dbt sl query コマンドでローカルからメトリクスを確認できるのは便利なんだけど、BI ツールとの連携設定は環境によってかなり差があるので注意が必要だと思う。焦って全部移行しようとするより、よく使うメトリクスから試した方が絶対いい。
Delta Lake・Iceberg・Hudi比較2026 でも書いたけど、データプラットフォーム全体の設計と合わせて考えないと、dbt だけ良くなっても意味ないのよね。うちは Iceberg + BigQuery の構成なので、dbt-bigquery 2.1 の Iceberg テーブルサポートがかなり助かってる。
それから、Apache Spark の記事 でも触れてたけど、大規模バッチでは dbt Python models を使う場面も出てきた。ただこれは dbt-spark / dbt-databricks 経由だとそれなりに癖があるので、SQL モデルで解決できるなら無理に Python models を使わない方がいいと思ってる。デバッグがしんどいし、トラブルシュートに時間を取られるリスクのわりにメリットが見えにくい場面が多かった。
まとめ
dbt 1.9→2.0 移行と半年の本番運用で学んだことを振り返ると、こんな感じにまとめられる。
run_results.jsonなど内部スキーマの変更は移行前に必ずチェック。「動いてるように見えるけど通知が飛んでない」という静かな地雷が一番怖い- モデル設計は
stg_/int_/mart_の役割を厳密に決める。Intermediate 層の設計が甘いと、重複ロジックが増えて数字の乖離事故につながる - Unit Testing を CI の第一段階に組み込む。実データに依存しないのでスピードが速く、ロジックのバグを早期発見できる
- Slim CI(
state:modified++--defer)で PR ごとの CI 時間を大幅短縮できる。ただし manifest.json のバージョンには注意 - Model Contracts は mart 層に積極的に入れる。BI ツールや API が依存するモデルのスキーマ変更事故ゼロになった
次のアクション:
- dbt 2.0 移行を検討してる方は、まず
dbt debugとdbt parseを 2.0 系でかけてエラーを洗い出すところから始めるといい - Semantic Layer は全社展開より「高頻度で使われるメトリクス 10〜20 個」から試すのがおすすめ
- Unit Testing は既存プロジェクトでも後から追加しやすい。まずビジネスロジックが複雑な
int_層から書き始めると効果を実感しやすい
半年やってみての感想は、「dbt は設計の規律がそのまま品質に直結するツール」だということ。SQL を書く自由度が高い分、チームのルールをしっかり決めないとカオスになる。逆にルールが整うと、データエンジニア以外のメンバーも「モデルを読めば何をしてるか分かる」状態になって、コミュニケーションコストが下がった。そこは素直によかったと思う。