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.ymlconfig-version: 2 廃止、構造変更あり
Deprecationrun_results.json のスキーマ変更
Unit Testingdbt testdbt test --select unit_tests で分離
Semantic LayerMetricFlow の統合強化、YAML定義が刷新
Python modelsdbt-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 層を真剣に設計し直した。正直、もっと早くやるべきだったと思う。

今のチームのルールはこうなってる。

命名役割ビジネスロジック
Stagingstg_ソース1つに対して1モデル。型変換・カラム名の標準化のみ❌ 入れない
Intermediateint_複数ソースの結合、ビジネスロジックの分解✅ ここが主戦場
Martmart_BIツール・ダウンストリームが直接参照。集計・絞り込みのみ△ 最小限

個人的には int_ の設計が一番重要だと思ってる。ここを雑にすると全部が雑になる。命名は int_{ドメイン}__{動詞+名詞} で統一していて、int_payments__attributed みたいな感じ。ファイル名だけで「このモデルが何をしているか」が分かるのが地味に快適。

dbt テスト戦略、正直やりすぎた話と2026年の落とし所

テストは最初張り切りすぎた。全カラムに not_nullunique を書いて、さらに 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 移行と半年の本番運用で学んだことを振り返ると、こんな感じにまとめられる。

  1. run_results.json など内部スキーマの変更は移行前に必ずチェック。「動いてるように見えるけど通知が飛んでない」という静かな地雷が一番怖い
  2. モデル設計は stg_ / int_ / mart_ の役割を厳密に決める。Intermediate 層の設計が甘いと、重複ロジックが増えて数字の乖離事故につながる
  3. Unit Testing を CI の第一段階に組み込む。実データに依存しないのでスピードが速く、ロジックのバグを早期発見できる
  4. Slim CI(state:modified+ + --defer)で PR ごとの CI 時間を大幅短縮できる。ただし manifest.json のバージョンには注意
  5. Model Contracts は mart 層に積極的に入れる。BI ツールや API が依存するモデルのスキーマ変更事故ゼロになった

次のアクション:

  • dbt 2.0 移行を検討してる方は、まず dbt debugdbt parse を 2.0 系でかけてエラーを洗い出すところから始めるといい
  • Semantic Layer は全社展開より「高頻度で使われるメトリクス 10〜20 個」から試すのがおすすめ
  • Unit Testing は既存プロジェクトでも後から追加しやすい。まずビジネスロジックが複雑な int_ 層から書き始めると効果を実感しやすい

半年やってみての感想は、「dbt は設計の規律がそのまま品質に直結するツール」だということ。SQL を書く自由度が高い分、チームのルールをしっかり決めないとカオスになる。逆にルールが整うと、データエンジニア以外のメンバーも「モデルを読めば何をしてるか分かる」状態になって、コミュニケーションコストが下がった。そこは素直によかったと思う。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事