Claude Media
Claude Codeでdbt runが失敗した原因を特定する — run_results.json活用

Claude Codeでdbt runが失敗した原因を特定する — run_results.json活用

dbt runが落ちたとき、run_results.jsonからerrorのノードだけをjqで抜き、コンパイル済みSQLと突き合わせてClaude Codeに原因を絞らせる手順をまとめます。

dbt runが失敗したとき、ターミナルのログ全体をClaude Codeに貼ると、入力が膨らむ割に原因は絞れません。失敗の事実はログではなくtarget/run_results.jsonにまとまっています。errorになったノードだけをjqで抜き、そのノードのコンパイル済みSQLと並べてから渡すと、Claudeが読む量は数十行に収まります。この記事では、抜き出しから原因の分類、claude -pでの自動化、再実行の判断までを手順にします。

失敗の一次情報はrun_results.jsonにある

dbtは実行のたびに成果物(artifact)を書き出します。既定の出力先はプロジェクトのtarget/ディレクトリで、--target-pathで変えられます。run・build・test・compileなどの実行系コマンドは、そのうちrun_results.jsonを生成します。

このファイルのresults配列には、実行されたノードごとに次のキーが入ります。調査で使うものに絞ります。

  • unique_id: model.my_project.my_modelのようなノードの識別子
  • status: 成功、失敗、エラーのどれかをdbtが解釈した値
  • message: dbtがCLIに出すのと同じ報告文。データベースが返した内容に基づく
  • compiled_code: Jinjaを展開した後の、実際に投げたSQL
  • relation_name: 作成・更新しようとしたオブジェクトの完全修飾名
  • adapter_response: データベースが返したメタデータ。アダプターごとに中身が違う

押さえておきたいのは「実行されたノードだけが載る」点です。別の条件でdbt runとdbt testを続けて回すと、それぞれが別のrun_results.jsonを作ります。調べたい失敗の結果を残すなら、再実行の前にコピーを取っておきます。

statusの値は、公式の例にsuccessとpassが出てくるだけで、取りうる値の一覧はこのページにありません。そのため抽出は「errorだけ」と決め打つより、「成功以外すべて」の除外形で書くほうが取りこぼしません。

errorのノードだけをjqで抜く

まず失敗したノードの一覧を出します。

jq -r '.results[]
  | select(.status != "success" and .status != "pass")
  | [.status, .unique_id] | @tsv' target/run_results.json

手元で作った小さなサンプル(3ノード)にこのフィルタを当てると、次の形で返ります。

error	model.shop.fct_orders
skipped	model.shop.fct_daily

errorが原因のノードで、skippedはその影響を受けただけです。調べるのはerrorの側です。上流の失敗で下流がskippedになる挙動は、dbt buildの選択と再実行を扱った記事でも触れています。

次に、errorのノードからmessageとcompiled_codeだけを取り出します。ここが、Claudeに渡す最小の材料です。

jq '[.results[]
  | select(.status == "error")
  | {unique_id, relation_name, message, compiled_code}]' \
  target/run_results.json > /tmp/dbt-failures.json

サンプルでは、messageにDatabase Error in model fct_orders (models/marts/fct_orders.sql)に続けてcolumn "order_total" does not existが入り、compiled_codeにはselect order_total from ...が入ります。エラー文は「列がない」と言っており、SQLは確かにその列を参照している。突き合わせの価値は、この2つを同時に見られることです。

compiled_codeとソースSQLのどちらを読むか

dbtのモデルはJinja入りのSQLです。ソースのmodels/marts/fct_orders.sqlを読んでも、ref()やsource()、マクロ、is_incremental()の分岐が展開された後の姿は分かりません。データベースが実際に受け取ったのは展開後のSQLなので、エラー文の「何行目」「どの列」もそちらに対応します。

run_results.jsonのcompiled_codeは、その展開後の文字列です。ファイルとしてもtarget/compiled以下に書き出されます。データテストのコンパイル済みクエリも同じ場所で確認できます。

Claudeに渡す内容は2層に分けると読みやすくなります。

  • 第1層: messageとcompiled_code(上のdbt-failures.json)。エラー文と実際のSQLの突き合わせに使う
  • 第2層: 第1層で見当がついた後に開く、ソースのモデルファイルと参照先モデルの定義。直す場所を決めるために使う

最初から第2層を全部読ませると、無関係なモデルまでコンテキストに入ります。第1層で原因の種類を決めてから、必要なファイルだけを開かせます。

原因は4つの型に分けて考える

messageとcompiled_codeの組み合わせから、原因はおおよそ4つに分かれます。Claudeに分類させるときの見取り図にもなります。

切り分け

エラー文とSQLから見分ける原因の型

  • 参照先の不整合

    列名・テーブル名がないというエラー。compiled_codeの参照先(relation_nameの上流)を見て、上流モデルが列を出しているか確かめます。

  • SQL構文・型の誤り

    構文エラーや型の不一致。展開後のSQLを読むと、Jinjaの出力ミスで,や括弧が欠けているのが見えることがあります。

  • 権限・接続

    スキーマやテーブルへの権限エラー。ノード自体は無実で、プロファイルのロールが原因です。compiled_codeではなくmessageが決め手になります。

  • データの問題

    ユニーク制約違反や型変換の失敗など、入力データに依存するもの。同じSQLでも日によって通ったり落ちたりします。

どの型かで次の一手が変わります。参照先と構文はコードの修正、権限は人間への確認、データは上流の調査です。権限エラーをClaudeにSQLの修正で解決させようとすると、ループに入りがちです。「権限に見えるときはコードを書き換えず報告する」と先に決めておきます。

claude -pで原因分析を一発で出す

抽出したdbt-failures.jsonを標準入力で渡し、claude -pに分析させます。公式のヘッドレス実行のページには、データをパイプで渡す使い方があり、Claudeに読み取りの権限を与えなくても入力を読ませられます。

cat /tmp/dbt-failures.json | claude -p \
  "dbt runが失敗したノードのmessageとcompiled_codeです。
各ノードについて、原因を『参照先の不整合/構文・型/権限・接続/データ』の
どれかに分類し、根拠のSQLの該当箇所を示してください。
権限・接続に見える場合はコードを修正せず報告のみにしてください。"

結果をスクリプトで処理したいときは、--output-format jsonに--json-schemaを組み合わせます。出力がスキーマに沿った形でstructured_outputフィールドに入ります。

cat /tmp/dbt-failures.json | claude -p "原因を分類してください" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"nodes":{"type":"array","items":{"type":"object","properties":{"unique_id":{"type":"string"},"cause":{"type":"string"},"evidence":{"type":"string"}},"required":["unique_id","cause"]}}},"required":["nodes"]}' \
  | jq '.structured_output'

注意点が1つあります。Claudeが有効な構造化出力を返せなかったとき(依頼を断った場合など)、structured_outputフィールド自体が付きません。後続の処理は、このフィールドが存在することを確かめてから使います。

モデルファイルまで読ませて修正案を出させる段階では、権限を明示します。Readに加えて、読み取りに使うコマンドだけを許可し、書き込み系のdbtコマンドは許可しません。

claude -p "target/run_results.jsonのerrorのノードを調べ、直す場所を提案して。
ファイルは編集せず、dbt runも実行しないこと" \
  --allowedTools "Read,Bash(jq *),Bash(dbt compile *)" \
  --permission-mode dontAsk

dontAskは、確認が必要になる呼び出しをすべて拒否するモードで、無人実行向けです。--allowedToolsに載せたものと、承認不要の読み取りだけが動きます。v2.1.302以降のclaude -pは、モードの指定がなければ通常の確認ありモードで始まるため、この指定でブレを消せます。

CLAUDE.mdに調査の手順を書いておく

毎回プロンプトに書くより、調査の手順をCLAUDE.mdに置いたほうが安定します。次はその断片の一例です。パスは自分のプロジェクトに合わせます。

## dbtの失敗調査
- 失敗したら、まず `target/run_results.json` を読む。ターミナルのログ全体は読まない
- `status` が success と pass 以外のノードを列挙し、error のものから調べる
- `message` と `compiled_code` を突き合わせてから、ソースのモデルを開く
- 原因を「参照先の不整合 / 構文・型 / 権限・接続 / データ」に分類して報告する
- 権限・接続が疑われるときはコードを直さず、人間に確認する
- 修正後は失敗したノードだけを再実行する(全体の `dbt run` は禁止)

この手順の肝は、ログを読ませない点と、権限系を人間に戻す点です。ここが決まっていると、Claudeは「とりあえずSQLを書き換える」方向に進みません。

データテストの失敗は別の読み方をする

dbt testやdbt buildでテストが落ちた場合も、run_results.jsonにノードとして残ります。ただし原因は、モデルのSQLよりデータの中身にあることがあります。テストのクエリは「失敗した行を1行ずつ返す」形で書かれており、返った行数が失敗件数です。

どの行が失敗したのかを見るには、--store-failuresフラグ、またはstore_failures・store_failures_asの設定を使います。dbtがテストの結果をデータベースのテーブルに保存するため、Claudeに実データの失敗行を調べさせられます。保存されたテーブルの扱いには注意が必要です。同じテストの失敗は、次の実行で上書きされます。

修正後はdbt retryで失敗箇所から再開する

原因を直したら、全体を回し直す必要はありません。dbt retryは、直前の実行の失敗した地点から再開します。参照するのはrun_results.jsonです。

dbt retry

挙動には3つの条件があります。

  • 失敗前に1ノードも実行されていなければ、retryは何もしない。接続エラーや権限エラーで早期に落ちた場合がこれで、run_results.jsonを確認して全体を手動で再実行する
  • 直前の実行が成功していた場合は、no operationで終わる
  • 失敗の原因を直さずに実行すると、結果は同じになる(冪等)

dbt v2.0以降ではdbt retry --select my_model+のように、再実行の範囲を絞れます。ただしv1系では、選択は直前の実行を引き継ぎます。使っているバージョンをdbt --versionで確認してから使い分けます。

つまり、接続や権限のエラーで1ノードも実行されずに落ちたときは、dbt retryが何もしません。分類が「権限・接続」だったときは、直したあとに手動で全体を回し直します。

よくあるつまずき

  • 古いrun_results.jsonを読んでいる: 別のdbtコマンドを実行した後は、ファイルの中身がそのコマンドの結果に変わる。調べる対象の実行直後にコピーを取っておく
  • compiled_codeが空: 解析だけで終わったノードにはコンパイル済みコードが入らない(parse後は空、compile後に入る)。コンパイル前に落ちたノードは、messageだけを頼りにする
  • ログも見たい: ログはlogs/dbt.logに書かれる。--log-pathで場所を変えられ、DEBUGレベルを端末にも出すこともできる。run_results.jsonで足りないときの補助に使う
  • 成果物を介さず操作したい: DAGの実行やリネージの確認をツール呼び出しで済ませたいときは、dbt MCPサーバーの使い方が別にあります

まとめ

dbtの失敗調査は、ログを読ませるより成果物を絞って渡したほうが速く、再現もしやすくなります。errorのノードのmessageとcompiled_codeをjqで抜き、原因を4つの型に分けて報告させる。権限・接続は人間に戻す。これをclaude -pとCLAUDE.mdに落とせば、CIの失敗レポートにも、手元の調査にも同じ手順が使えます。非対話実行の権限設計はclaude -pオプションの解説に詳しくまとまっています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. docs.getdbt.com/reference/artifacts/run-results-json
  3. docs.getdbt.com/reference/artifacts/dbt-artifacts
  4. docs.getdbt.com/reference/commands/retry
  5. docs.getdbt.com/docs/build/data-tests
  6. docs.getdbt.com/reference/global-configs/logs
この記事を共有:XはてブLinkedIn