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を展開した後の、実際に投げたSQLrelation_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_dailyerrorが原因のノードで、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 dontAskdontAskは、確認が必要になる呼び出しをすべて拒否するモードで、無人実行向けです。--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オプションの解説に詳しくまとまっています。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。