ClaudeにSnowflakeのクエリが遅い原因を探させる — スピルと枝刈りの見つけ方
Query Profileの画面は渡せません。GET_QUERY_OPERATOR_STATSの出力をCSVにしてclaude -pに渡し、ディスクへのスピル、パーティション枝刈り、爆発する結合を読ませる手順です。
Snowflakeで遅いクエリの原因を絞るとき、最初に開くのはSnowsightのQuery Profileです。ただ、この画面はClaudeに渡せません。代わりに使うのが、同じ情報を表として返すGET_QUERY_OPERATOR_STATS関数です。
この関数の出力をCSVにして、claude -pにパイプで渡せば、ディスクへのスピル、パーティション枝刈りの効き具合、行数が膨らむ結合を、Claudeに拾わせられます。ここでは、渡せる形に整えるSQL、プロンプトの組み方、出力の読み方、Claudeに任せきれない境界までを順に書きます。
Query Profileの代わりに何を渡すか
GET_QUERY_OPERATOR_STATSは、完了したクエリの演算子ごとの統計を返すテーブル関数です。実行から14日以内のクエリが対象で、Snowsightのquery profileタブに出る情報を、プログラムから取れる形にしたものと説明されています。
使うための条件は3つあります。
- 完了したクエリだけが対象で、実行中のクエリには使えません
- クエリを実行したウェアハウスのOPERATEまたはMONITOR権限が要ります
FROM句でTABLE()に包んで呼びます
返る列は8つです。QUERY_ID、STEP_ID、OPERATOR_ID、PARENT_OPERATORS、OPERATOR_TYPEに加えて、VARIANT型のOPERATOR_STATISTICS、EXECUTION_TIME_BREAKDOWN、OPERATOR_ATTRIBUTESがあります。診断に効くのは後ろの3つです。
| 列 | 入っているもの | 診断での使いどころ |
|---|---|---|
OPERATOR_STATISTICS | 入っているもの入出力の行数、pruning、spilling、io | 診断での使いどころ枝刈り、スピル、行数の膨張 |
EXECUTION_TIME_BREAKDOWN | 入っているもの演算子ごとの時間配分(overall_percentageなど) | 診断での使いどころどの演算子が時間を食ったか |
OPERATOR_ATTRIBUTES | 入っているものテーブル名、結合条件、フィルタ条件など | 診断での使いどころどのテーブル・条件の話か |
注意したいのは、出力にクエリ文が含まれない点です。列の一覧にQUERY_TEXTのような列はありません。演算子の統計だけを渡しても、Claudeはどのテーブルをどう書き換えればよいかまで言えないので、SQL本文は別に添えます。
CSVで渡せる形に平たくするSQL
関数の出力のままでは、3列がJSONの塊です。CSVにすると、波括弧の中身が1セルに押し込まれ、Claudeは数字を探すだけで読み取り量を使います。診断に要る項目だけを列に開いてから書き出します。
次のSQLは、公式ページの列・キー名に沿った一例です。'<query_id>'には、診断したいクエリのIDを入れます。
SELECT
operator_id,
operator_type,
operator_attributes:table_name::string AS table_name,
execution_time_breakdown:overall_percentage::float AS overall_pct,
execution_time_breakdown:local_disk_io::float AS local_disk_io_pct,
execution_time_breakdown:remote_disk_io::float AS remote_disk_io_pct,
operator_statistics:input_rows::number AS input_rows,
operator_statistics:output_rows::number AS output_rows,
operator_statistics:output_rows
/ NULLIF(operator_statistics:input_rows, 0) AS row_multiple,
operator_statistics:pruning:partitions_scanned::number AS parts_scanned,
operator_statistics:pruning:partitions_total::number AS parts_total,
operator_statistics:spilling:bytes_spilled_local_storage::number AS spill_local,
operator_statistics:spilling:bytes_spilled_remote_storage::number AS spill_remote
FROM TABLE(GET_QUERY_OPERATOR_STATS('<query_id>'))
ORDER BY execution_time_breakdown:overall_percentage DESC NULLS LAST;row_multipleは、出力行数を入力行数で割った値です。公式ページが「爆発する結合」を探す例で使っている計算と同じ形です。並び順は時間の占有率の降順にしてあるので、CSVの上から順に読めば、重い演算子から見られます。
この結果をCSVに書き出す手段は、お使いのクライアントに任せます。Snowsightの結果表からのダウンロードでも、SQLクライアントの出力指定でも、列が上のとおり並んでいれば足ります。
空欄の扱いは、プロンプトで決めておきます。列に情報が無い演算子はNULLになります。空欄が「ゼロだった」のか「記録が無い」のかをClaudeに推測させると、結論がぶれます。「空欄は未記録として扱い、ゼロとは読まない」と一文書いておきます。
claude -pに渡して所見を出させる
ファイルが用意できたら、標準入力にまとめて流し込みます。Claude Codeの非対話モードは標準入力を読めるので、catの出力をパイプでつなげます。
{ cat slow_query.sql; echo; cat operator_stats.csv; } | claude -p \
"上はSnowflakeのクエリ文と、そのクエリのGET_QUERY_OPERATOR_STATSを\
平らにしたCSVです。overall_pctの大きい演算子から順に、次の3点を調べて\
ください。(1)spill_local / spill_remoteが空でも0でもない演算子\
(2)TableScanでparts_scannedがparts_totalに対して大きい演算子\
(3)JoinでSTEP内のrow_multipleが1を大きく超える演算子。\
空欄は未記録として扱い、0とは読まないこと。数字の根拠を\
operator_idで示し、推測は推測と書くこと"標準入力の上限は10MBです。超えるとClaude Codeはエラーを出して非ゼロで終了します。演算子数が数百でも、平らにしたCSVなら数十KBに収まるので、上限に近づく場面は多くありません。巨大な出力を渡したくなったら、ファイルに書いてパスをプロンプトで指す方法に切り替えます。
この形にしておくと、権限の設定も要りません。診断対象はパイプで渡した文字列だけなので、ClaudeにBashやファイル読み取りの許可を与えなくて済みます。公式ページも、差分をパイプで渡す例について同じ利点を説明しています。
結果を機械で扱えるJSONで受け取る
所見を後続の処理に流したいなら、--output-format jsonと--json-schemaを組み合わせます。スキーマに合う結果はstructured_outputフィールドに入ります。
スキーマは長いので、ファイルに置いて読み込ませます。
{
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"operator_id": {"type": "integer"},
"kind": {
"type": "string",
"enum": ["spill", "pruning", "join_explosion", "other"]
},
"evidence": {"type": "string"}
},
"required": ["operator_id", "kind", "evidence"]
}
}
},
"required": ["findings"]
}{ cat slow_query.sql; echo; cat operator_stats.csv; } | claude -p \
"スピル・枝刈り・結合の膨張の所見を返してください" \
--output-format json \
--json-schema "$(cat findings.schema.json)" \
| jq '.structured_output'有効な構造化出力が得られなかった実行では、structured_outputフィールド自体がありません。依頼を断られたときなどが当たります。後続の処理に流すスクリプトは、フィールドの有無を先に確かめてから使います。
evidenceに根拠の数字を入れさせると、あとで人が元のCSVと突き合わせるときに、どの行を見ればよいかが分かります。
出力の読み方 — スピル・枝刈り・結合の膨張
Claudeに読ませる前に、読み方の基準を人が持っておくと、返ってきた所見の良し悪しが判断できます。Query Profileのページが挙げる典型的な問題のうち、この関数の出力から追えるものを並べます。
| 症状 | 見る列 | 公式ページの示す方向 | Claudeに頼むこと |
|---|---|---|---|
| ディスクへのスピル | 見る列spill_local、spill_remote | 公式ページの示す方向ウェアハウスを大きくする、データを小分けに処理する | Claudeに頼むことどの演算子で出ているかの特定 |
| 枝刈りが効かない | 見る列parts_scannedとparts_total | 公式ページの示す方向データの格納順とフィルタ列を揃える | Claudeに頼むこと対象テーブルとフィルタ条件の突き合わせ |
| 結合の膨張 | 見る列row_multiple | 公式ページの示す方向結合条件が正しいか確かめる | Claudeに頼むこと結合条件の読み解き |
| UNIONの重複排除 | 見る列UnionAllの上のAggregate | 公式ページの示す方向重複が無いならUNION ALLで足りる | Claudeに頼むことSQL上のUNIONの洗い出し |
スピル
メモリに収まらない中間結果は、まずローカルディスクへ、ローカルも足りなければリモートディスクへ書き出されます。リモートへのスピルは、性能への影響が特に大きいとされています。
spill_remoteが空でも0でもない演算子があれば、最優先で見ます。同じ演算子でremote_disk_io_pctも大きければ、待ち時間の内訳からも、スピルの影響を裏づけられます。示されている対処は、より大きなウェアハウスの利用と、データを小分けにして処理することの2つです。
Claudeに頼めるのは、「どの演算子で、どのくらいスピルしたか」の特定までです。ウェアハウスのサイズを上げるかどうかは、コストとの兼ね合いで人が決めます。小分けにする案は、クエリ文を一緒に渡してあれば、期間やキーで分割する書き換えの下書きまで出せます。
枝刈り
Snowflakeは、クエリのフィルタに基づいて、読まなくてよいパーティションを読み飛ばします。ただし、格納順がフィルタの列と相関していないと効果が出ません。
効き具合は、TableScan演算子のpartitions_scannedとpartitions_totalの比で見ます。前者が後者の小さな割合なら、枝刈りは効いています。大きな割合なら、効いていません。
落とし穴
比が大きくても問題とは限らない
枝刈りが効くのは、データを大きく絞り込むクエリだけです。もともと全件に近い範囲を読むクエリでは、比が1に近くても自然です。判断の手がかりは、TableScanの上にFilter演算子があり、そこで多くの行が落ちているかどうかです。スキャンは広いのにフィルタで大量に捨てている形なら、データの並べ方を見直す余地があるというのが、Query Profileのページの見立てです。
プロンプトに「フィルタ条件の無いスキャンは枝刈りの対象外として扱う」と書いておくと、Claudeが全件スキャンを不具合と報告する誤りを減らせます。Filter演算子の条件はOPERATOR_ATTRIBUTESのfilter_conditionに入るので、必要ならCSVに列を足して渡します。
結合の膨張
結合条件が抜けている、あるいは1行が相手の複数行に当たる条件になっていると、Join演算子は入力より桁違いに多い行を出力します。関数のページの例では、4つのテーブルを結合するクエリで、結合ごとのrow_multipleが約50倍、約116倍、約12倍と並んでいました。
膨張が見つかったら、その結合の条件を確かめるのが次の一手です。ここは、クエリ文とequality_join_conditionを並べて読ませると、Claudeに任せやすい作業です。条件に使っている列が一意でないことを、スキーマ情報なしに断定させるのは避けます。テーブルの定義かCOUNT(DISTINCT ...)の結果を足して渡します。
所見をそのまま信じないための確かめ方
Claudeの所見は仮説です。次の3点を押さえておくと、仮説と事実を取り違えにくくなります。
- 同じクエリでも実行ごとに数字が変わる。同じ実行IDに対して関数は同じ値を返しますが、クエリ文が同じでも別の実行なら統計は変わります。データ量、マテリアライズドビューの状態、クラスタリングの有無、キャッシュの状態、ウェアハウスのサイズが影響します。
percentage_scanned_from_cacheが高い実行と低い実行を、同列に比べないようにします。 - 直したら同じ手順で測り直す。ウェアハウスを変える、書き換えるなどした後は、新しいクエリIDで同じSQLを流し、CSVを作り直して、同じプロンプトで読ませます。
overall_pctの上位やspill_remoteが実際に動いたかで効果を判断します。 - 属性にはクエリの中身が入る。
OPERATOR_ATTRIBUTESには、フィルタ条件、結合条件、VALUES句の値などが入ります。CSVにそれらを含めると、条件式の中身ごとClaudeに送ることになります。個人情報や機密値を条件に書くクエリでは、属性の列を落とすか、値を伏せてから渡します。
毎回同じ観点で読ませる
診断の観点は、毎回プロンプトに書くより、ファイルに固定しておくほうが安定します。CLAUDE.mdに、このCSVの列の意味と読み方の基準を書いておく方法があります。
## Snowflake演算子統計CSVの読み方
- 列: operator_id, operator_type, table_name, overall_pct, ...
- 空欄は未記録。0とは読まない
- スピルはspill_remoteを優先して報告する
- フィルタ条件の無いTableScanは枝刈りの対象外として扱う
- 結論には必ずoperator_idを添えるCIや定期ジョブから呼ぶ場合は、--bareを付けるとCLAUDE.mdを含めて読み込まずに起動するので、この方法は効きません。その場合は、観点をプロンプトの本文に入れて渡します。
接続して直接読ませる方法との違い
SnowflakeにClaudeを直接つなぎ、Claude自身にこの関数を叩かせる構成もあります。接続の準備と権限の考え方はClaude Snowflake連携の設定手順にまとめています。Cortex Codeに依頼を回すプラグインを使う構成は、Claude CodeからCortex Codeへ依頼を転送するプラグインが扱っています。
この記事の方法は、接続を作らず、CSVという最小限の情報だけを渡す点が違います。ウェアハウスの権限を持つ人が自分の手元でSQLを流し、結果のCSVだけをClaudeに見せるので、Claude側にSnowflakeの認証情報を持たせずに済みます。CockroachDBのクエリ診断をプラグイン経由で任せる流れはClaude CodeにCockroachDBプラグインを入れる記事にあります。データベースごとに、渡し方の選択肢が変わります。
まとめ
Query Profileの画面は渡せませんが、GET_QUERY_OPERATOR_STATSの出力を列に開いたCSVにすれば、Claudeは同じ情報を読めます。SQL本文を添えること、空欄の扱いを決めること、演算子IDで根拠を示させることの3点が、所見の質を左右します。
Claudeに任せやすいのは、数字の拾い出しと、結合条件やフィルタ条件の読み解きです。ウェアハウスのサイズ変更や再クラスタリングのような費用が絡む判断は、所見を材料に人が決めます。直したあとは新しいクエリIDで測り直し、スピルと枝刈りの数字が動いたかで効果を確かめます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。