dbt運用トラブルあるある9選!

🚀 dbt運用トラブルあるある9選!ムダな残業を減らす対策ガイド

このガイドは、dbt(データビルドツール)をいじっていると、「あー、またやっちゃった!」 となりがちな9つの困った事例と、その具体的な避け方・直し方をまとめた、超実用的なマニュアルです。


🧪 1. テストやりすぎて、パイプラインが止まる&お金がかかる

発生概要

とりあえず「全部のカラムにテストつけとけ!」ってやると、BigQueryとかSnowflakeでクエリが多すぎてエラーになったり、パイプラインが時間切れで止まったり、気づかないうちにお金がかさみます。

対応策 (Code Example)

🚨 大事なモデルだけテストするルールにしましょう。タグ付けして、賢くテストを実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# models/mart/core_metrics.yml
models:
  - name: final_daily_sales
    config:
      tags: ["core", "daily"] # 命綱モデルにタグを貼る!

# 実行コマンド:
# coreタグがついたモデルのテストだけ動かす
dbt test --select tag:core 

# 変更したモデルとその影響範囲だけテストする
dbt test --select state:modified+

🧩 2. モデルがぐちゃぐちゃ!誰にも分からないスパゲッティ状態

発生概要

最初は簡単だったのに、モデルを増やしていくうちに、どこで何をしているのかさっぱり分からなくなります。ちょっと変えるだけで、関係ないモデルまで壊れるという恐怖の状態。

対応策 (Code Example)

🌳 stg/int/mart の3層構造を絶対守りましょう。特に int_ 層にビジネスの計算ロジックを集めるのがコツです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- int_層のモデルの例:
-- stgから整形済みのデータを持ってきて、ビジネス上の重要な計算(ここでは初回購入日)をバッチリやる
SELECT
  u.user_id,
  u.user_name,
  MIN(o.order_date) AS first_purchase_date,
  COUNT(o.order_id) AS total_orders
FROM {{ ref('stg_ecommerce__users') }} AS u
LEFT JOIN {{ ref('stg_ecommerce__orders') }} AS o
  ON u.user_id = o.user_id
GROUP BY 1, 2

⚡ 3. コスト爆上がり&処理が激遅(全部読み込みすぎ問題)

発生概要

データが増えてくると、クエリの実行時間がどんどん長くなり、月次のクラウド費用を見てびっくり!ほとんどの原因は、テーブル全体をムダに読み込みすぎていることです。

対応策 (Code Example)

デカいテーブルは必ず incremental モデルにして、増えたデータだけ処理するように設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- incremental モデルの基本構造
{{ config(
    materialized='incremental', 
    unique_key='transaction_id', # データが重複しないようにキーを指定
    # DW側でパフォーマンスを上げる設定 (: BigQueryのパーティション)
    partition_by={
        "field": "created_at",
        "data_type": "timestamp"
    }
) }}

SELECT * FROM {{ source('raw_data', 'transactions') }}
{% if is_incremental() %}
  -- ココが重要!前回動かした時以降のデータだけを対象にする
  WHERE created_at > (SELECT MAX(created_at) FROM {{ this }})
{% endif %}

💣 4. full refreshの誤爆でデータ全消去

発生概要

dbt run --full-refresh は、テーブルを一旦まっさらに消してから作り直すという、破壊力抜群のコマンドです。これを本番でうっかり実行すると、データが吹き飛ぶ大惨事になります。

対応策 (Code Example)

本番環境では full-refresh を禁止し、スキーマが変わっても勝手にDROPさせないように設定しておきましょう。

1
2
3
4
5
6
# dbt_project.yml で設定
models:
  +post_hook: "ANALYZE TABLE {{ this }} COMPUTE STATISTICS" # 例: 実行後の最適化
  +on_schema_change: fail # ここを「fail」に!スキーマが変わったら、とりあえず止める(DROPさせない)

# profiles.yml で環境を分ける設定をして、本番(prod)では権限を絞る

🚫 5. マクロ/フックの使いすぎで、中身がブラックボックス化

発生概要

マクロ(Macros)は便利だけど、複雑な計算ロジックをマクロの中に全部隠してしまうと、モデルのSQLファイルはスッキリしても、裏側で何が動いているか誰も分からなくなって、デバッグが地獄になります。

対応策 (Code Example)

マクロは「何度も使う共通処理」(例: タイムスタンプの変換)だけに限定し、ビジネスの肝となるロジックはSQLモデルに残しておきましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 良い例: マクロを共通の型変換に利用
-- macros/timestamp_to_jst.sql
{% macro timestamp_to_jst(column_name) %}
  CONVERT_TZ({{ column_name }}, 'UTC', 'Asia/Tokyo')
{% endmacro %}

-- モデルでの使用:
SELECT
  user_id,
  {{ timestamp_to_jst('created_at') }} AS created_at_jst,
  ...
FROM {{ ref('stg_users') }}

⚙️ 6. 開発環境と本番環境で挙動が違う問題

発生概要

開発環境(dev)ではうまくいったのに、本番(prod)にデプロイすると突然エラーになったり、計算結果が違ったりします。これは、環境ごとの設定やデータ量、スキーマ名がズレているのが原因です。

対応策 (Code Example)

profiles.yml で環境を分けたら、モデルの設定で Jinjaを使って自動で設定を切り替えるようにしましょう。

1
2
3
4
5
6
# dbt_project.yml または models/config
models:
  # 本番のときだけincremental(増分)に、それ以外はtable(全構築)にする
  +materialized: "{% if target.name == 'prod' %}incremental{% else %}table{% endif %}"
  # ターゲット名に合わせてスキーマ名も自動で切り替える
  +schema: "{{ target.schema }}" 

🔄 7. スナップショット(履歴管理)未導入で過去データが追えない

発生概要

「顧客の名前が変わりました」という時、その変更を記録していないと、過去のデータを「名前変更前の状態」で集計し直すことができません。分析結果の信頼性がガクッと落ちます。

対応策 (Code Example)

変わる可能性のある重要なマスタデータ(ディメンション)は、dbt snapshot で自動的に履歴を追跡させましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{% snapshot customers_snapshot %}

{{
    config(
      target_schema='snapshots',
      unique_key='customer_id', # 追跡したいデータのID
      strategy='check',         # 変更を検出する方法を指定
      check_cols=['first_name', 'email'], # 履歴を記録したいカラムを指定
      invalidate_hard_deletes=True
    )
}}

SELECT * FROM {{ source('raw_data', 'customers') }}

{% endsnapshot %}

📝 8. ドキュメント(YAML)スカスカで、モデルの中身が謎

発生概要

models/schema.ymlモデルやカラムの説明をちゃんと書いていないと、後からコードを見た人が「このカラム、何の意味?」となり、誰も触れないレガシーモデルになっていきます。

対応策 (Code Example)

モデルと全カラムに必ず description を書くことを、Pull Requestのルールに組み込みましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# models/mart/final_daily_sales.yml
version: 2
models:
  - name: final_daily_sales
    description: "BIツールで使う、日次売上集計の最終形です。" # 必ず説明を書く!
    columns:
      - name: day_key
        description: "売上日のキー(YYYYMMDD形式)。"
        tests:
          - unique
          - not_null
      - name: total_sales_usd
        description: "その日の総売上(米ドル)。マイナスになることはないはず。"
        tests:
          - dbt_expectations.expect_column_to_be_between:
              min_value: 0
              max_value: 99999999

💾 9. seed の間違った使い方(デカいファイルを突っ込む)

発生概要

dbt seed は、小さなマスタ設定データ(CSV)を読み込むための機能です。これを何百メガバイトもあるデカいデータや、しょっちゅう更新されるデータに使ってしまうと、CI/CDパイプラインが重くなったり、データ管理がややこしくなったりします。

対応策 (Code Example)

seed小規模なマッピング表設定値に限定しましょう。デカいデータは、普通の dbt run を使って、ストレージから直接読み込むのが正解です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# dbt_project.yml
seeds:
  ja_dbt_project:
    # dbt seedを動かす時、config_dataフォルダにあるCSVだけを対象にする
    +select: ["config_data"]
    +enabled: true

# 実行コマンド:
# 小さな設定データだけをサクッとロードする
dbt seed 

🧰 まとめ: 運用トラブルと最適化のヒント

No.事象発生頻度主な原因ムダを減らすヒント
1テストのやりすぎ過剰テスト・リソース不足大事なモデルだけタグ付けしてテスト
2モデル構造がぐちゃぐちゃ設計ルールなし・依存増殖stg/int/mart の役割をガチで守る
3コスト爆上がりテーブル全体をムダにスキャンincrementalモデルで増分更新
4full refresh 誤爆設定ミス・操作ミス本番での --full-refresh 禁止on_schema_change: fail
5マクロ/フックの乱用ロジックが隠れる共通関数に限定し、ロジックはSQLに書く
6環境設定のズレtarget名の不統一・手動設定target.name を使って設定を自動で切り替え
7Snapshotで履歴が追えない変更履歴の記録なし重要なディメンションに dbt snapshot を導入
8ドキュメントが謎YAMLでの説明省略description は必須!レビューでチェック
9seed に巨大ファイルを突っ込むCI/CD時間の増加seed は小さな設定データ専用にする

✅ 運用チェックリスト(残業削減のために確認!)

  • テストは本当に大事なモデルに絞って、タグ管理してる?
  • stg/int/mart の役割、チーム全員が分かってる? int_ にロジック集まってる?
  • デカいテーブル、ちゃんと incrementalモデルになってる? unique_key はある?
  • 本番で full refresh を動かす権限、ちゃんと管理できてる?
  • 開発と本番で設定がズレないよう、Jinja で切り替えできてる?
  • 重要なマスタの変更履歴、dbt snapshot で追えてる?
  • 全モデル、全カラムに description 書いてある?
技術ネタ、趣味や備忘録などを書いているブログです
Hugo で構築されています。
テーマ StackJimmy によって設計されています。