今回の記事でやることは一言で言うとこれです。Claude Code に「直近で遅かったクエリTOP5は?」と日本語で聞くと、Claude が system.query_log を自分で掘ってレポートを返す。
分析用途と違って、運用診断は「調べるべきテーブルとカラムがだいたい決まっている(system.*)」「人間がやる調査手順がそのままプロンプトになる」ので、エージェントと相性がいい領域です。実際に手元の ClickHouse で動かしながら作っていきます。
この記事で作るもの
完成形のイメージはこんな感じです。Claude Code に話しかけるだけで、裏で SQL が組み立てられて実行されます。
> 直近1時間で遅かったクエリTOP5を、平均実行時間とピークメモリ付きで教えて
(Claude が system.query_log に対してSELECTを実行)
直近1時間で平均実行時間が長かったクエリは以下です。
1. avg 189ms / peak 464MiB SELECT user_id, count(), uniq(url) ... FROM events GROUP BY user_id
2. avg 182ms / peak 141MiB SELECT event_type, ..., avg(amount) FROM events LEFT JOIN orders ...
...
1番のクエリは1回あたり800万行を読んでメモリも一番使っています。
user_id での集約がボトルネックのようです。ORDER BY キーを確認しますか?
なお上の数値は、後述するローカルのデモ環境での実測値です。本番規模のDBではこれが秒オーダーになります。
ポイントは、Claude が1回のSQLで終わらず、結果を見て「次に何を調べるか」を自分で足していくところです。これは MCP のツールループ(クエリを投げる → 結果を読む → 必要なら追加クエリ)がそのまま「人間の調査手順」になっているからです。
仕組み(30秒)
構成はシンプルです。
[Claude Code] --(MCP)--> [mcp-clickhouse] --(HTTP)--> [ClickHouse]
└ system.query_log
└ system.parts
└ system.errors ...
- Claude Code: 普段コードを書かせているあのCLI。ここではDBの調査役にする
-
mcp-clickhouse: ClickHouse 公式の MCP サーバー。
SELECTを実行するツールなどを Claude に生やす -
ClickHouse の
system.*: 実行されたクエリの履歴やテーブルのディスク使用量など、運用情報が全部ここに溜まっている
system.* は ClickHouse が自動で記録してくれる運用メタデータの宝庫です。これを Claude に読ませるだけで「健康診断」になります。
セットアップ
1. ClickHouse を用意する
手元で試すだけなら Docker が一番早いです。
docker run -d --name clickhouse-health \
-p 8123:8123 -p 9000:9000 \
--ulimit nofile=262144:262144 \
-e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \
clickhouse/clickhouse-server:26.5
http://localhost:8123 に HTTP インターフェースが上がります。すでに運用中の ClickHouse がある人はそこに向けてもOKです(むしろ実データがある方が診断記事として面白い)。
イメージタグを :26.5 のように固定しておくと、この記事と同じバージョンで再現できます(latest のままだと、あとで別バージョンに変わります)。CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 は次のステップで CREATE USER を使うために付けています。公式イメージはデフォルトだとSQLによるユーザー作成・権限付与を許可しておらず、これがないと読み取り専用ユーザーを作る CREATE USER がエラーになります。
なお system.query_log は log_queries=1(デフォルト有効)でないと溜まりません。空っぽだった場合はこの設定を確認してください。立てたばかりの環境だと履歴がないので、何回かクエリを投げてから診断すると結果が出ます。
2. 読み取り専用ユーザーを作る(重要)
運用中のDBに Claude をつなぐので、事故防止が最優先です。MCP 側の設定だけに頼らず、DB 側でも読み取り専用ユーザーを切っておきます。二重に縛るのが安全です。
CREATE USER mcp_ro IDENTIFIED BY 'お好きなパスワード' SETTINGS readonly = 1;
GRANT SELECT ON *.* TO mcp_ro;
readonly = 1 にすると、このユーザーは SELECT 系しか実行できず、設定変更すらできなくなります(実際、readonly=1 の接続から SET max_threads=1 のような設定変更を投げると Cannot modify ... in readonly mode で弾かれます)。診断に必要なのは参照だけなので、これで十分です。
もう一段事故防止を固めるなら、暴走クエリ対策のリソース上限も付けておくと安心です。
ALTER USER mcp_ro SETTINGS
max_execution_time = 30,
max_rows_to_read = 1000000000,
max_memory_usage = 4000000000;
診断で触るのは system.* の軽いクエリばかりなので、この程度の上限で困ることはまずありません。逆に、うっかり巨大テーブルへ重いクエリを投げてしまっても、ここで頭打ちにできます。
3. mcp-clickhouse を Claude Code に登録する
Claude Code には claude mcp add でMCPサーバーを追加できます。公式の mcp-clickhouse は uv(Python)で動かすのが手軽です。
claude mcp add clickhouse \
-e CLICKHOUSE_HOST=localhost \
-e CLICKHOUSE_PORT=8123 \
-e CLICKHOUSE_USER=mcp_ro \
-e CLICKHOUSE_PASSWORD='お好きなパスワード' \
-e CLICKHOUSE_SECURE=false \
-- uvx mcp-clickhouse
ClickHouse Cloud に向ける場合は CLICKHOUSE_PORT=8443 / CLICKHOUSE_SECURE=true にします。
起動コマンド(uvx mcp-clickhouse の部分)や環境変数名、ツール名はバージョンで変わることがあるので(たとえば後述の run_query は、古い版では run_select_query という名前でした)、最新は公式リポジトリの README を確認してください。再現性を重視するなら uvx mcp-clickhouse@0.4.0 のようにバージョンを固定しておくと安心です。
登録できたら接続確認します。
claude mcp list
clickhouse が ✓ connected になっていればOKです。あとは Claude Code を起動して、/mcp でツールが見えているか確認しておくと安心です。
公式 mcp-clickhouse が生やすツールは、ざっくり次の3つです。
-
list_databases… データベース一覧 -
list_tables… テーブルとスキーマ -
run_query…SELECTの実行(デフォルトで読み取り専用)
この「読み取り専用がデフォルト」というのが、運用診断にちょうどいい性質です。
ここから先の出力例は、ClickHouse 26.5 を Docker で立て、datasets データベースに次のテーブルを入れた環境での実測値です。
-
events(800万行)/sessions(200万行)/orders(80万行)… 通常のテーブル -
events_raw(1.5万行)… わざとマージを止めて細かくINSERTし、150パーツに断片化させたテーブル
さらに「重いクエリ」「メモリ上限に当たるクエリ」「存在しないテーブルを引くクエリ」を数本流して、query_log / errors にネタを仕込んであります。
健康診断その1:重いクエリを探す
まずは定番、「最近どのクエリが重かったか」です。Claude Code にこう聞きます。
> 直近1時間で平均実行時間が長かったSELECTクエリTOP5を、
実行回数・平均ms・最大ms・ピークメモリ・読み込み行数つきで出して。
同じ形のクエリはまとめて。
「同じ形のクエリはまとめて」と言うと、Claude は normalized_query_hash(パラメータ違いを無視した正規化ハッシュ)で集約してくれます。生成・実行される SQL はだいたいこうなります。
SELECT
normalized_query_hash,
any(query) AS sample_query,
count() AS runs,
round(avg(query_duration_ms)) AS avg_ms,
max(query_duration_ms) AS max_ms,
formatReadableSize(max(memory_usage)) AS peak_mem,
sum(read_rows) AS total_read_rows
FROM system.query_log
WHERE event_time > now() - INTERVAL 1 HOUR
AND type = 'QueryFinish'
AND query_kind = 'Select'
GROUP BY normalized_query_hash
ORDER BY avg_ms DESC
LIMIT 5
実際に動かすと、こんな結果が返ってきました。
| runs | avg_ms | max_ms | peak_mem | total_read_rows | sample_query |
|---|---|---|---|---|---|
| 3 | 189 | 244 | 464.05 MiB | 24,000,000 | SELECT user_id, count() c, uniq(url) u, avg(duration_ms) d FROM datasets.events ... |
| 2 | 182 | 205 | 141.22 MiB | 17,600,000 | SELECT e.event_type, count(), avg(o.amount) FROM datasets.events e LEFT JOIN ... |
| 5 | 18 | 47 | 5.24 MiB | 40,000,000 | SELECT event_type, count() FROM datasets.events GROUP BY event_type |
面白いのが3番目の行です。5回で合計4,000万行(1回あたり800万行)も読んでいるのに、平均18msと速い。単純な count() でカラムをほとんど触らないからです。つまり「読み込み行数が多い=重い」とは限らず、avg_ms と peak_mem を併せて見るのが大事、というのがこの表から読み取れます。実際、いちばん重い1番(avg 189ms / peak 464MiB)は user_id での集約と uniq() がメモリを使っているクエリでした。
もうひとつ効いてくるのが type = 'QueryFinish' のフィルタです。system.query_log には1クエリにつき「開始(QueryStart)」と「終了(QueryFinish)」の複数行が入るので、これを外すと件数が二重に数えられます。Claude はだいたいこれを入れてきますが、入れ忘れることもあるので、出力が変なときは生成SQLを覗いて確認するクセをつけておくとよいです。
そのまま深掘りもできます。
> 1番のクエリ、毎回800万行フルスキャンしてる。
ORDER BY キーや集約の仕方で軽くできる余地ある?
Claude が SHOW CREATE TABLE でテーブル定義を確認し、ORDER BY キーと集約キーの相性や EXPLAIN を見にいきます。ここが「ツールループ」の本領で、人間が調査でやる「仮説 → 確認クエリ → 次の仮説」をそのまま代行してくれます。
健康診断その2:肥大テーブルとパーツの断片化
次は「どのテーブルがディスクを食っているか」「パーツが増えすぎて性能を落としていないか」です。ClickHouse 運用でいちばん効くのがこのパーツ周りです。
> ディスク使用量が大きいテーブルTOP10を、行数・パーツ数・圧縮率つきで。
あとアクティブパーツが100個を超えてるテーブルがあれば別で教えて。
ディスク使用量のランキングは、こんなSQLで出せます。
SELECT
database,
table,
formatReadableSize(sum(bytes_on_disk)) AS disk,
sum(rows) AS rows,
count() AS parts,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 1) AS compression_ratio
FROM system.parts
WHERE active
AND database = 'datasets'
GROUP BY database, table
ORDER BY sum(bytes_on_disk) DESC
LIMIT 10
手元で実行した結果がこちらです。
| database | table | disk | rows | parts | compression_ratio |
|---|---|---|---|---|---|
| datasets | events | 75.81 MiB | 8,000,000 | 12 | 2.9 |
| datasets | sessions | 43.68 MiB | 2,000,000 | 2 | 1.1 |
| datasets | orders | 13.01 MiB | 800,000 | 1 | 1.5 |
| datasets | events_raw | 253.45 KiB | 15,000 | 150 | 1.2 |
最後の events_raw に注目してください。たった15,000行なのにパーツが150個もあります。これがあとで効いてくる断片化のサインです。active の絞り込みを忘れると、マージ済みで消える予定の古いパーツまで数えてしまい、ディスク使用量を過大に見積もります。compression_ratio(無圧縮 ÷ 圧縮後)も一緒に出しておくと、「このテーブルは圧縮が効いていない=カラム設計を見直す余地がある」といった気づきにつながります(events は2.9倍効いている一方、sessions はほぼ1.1倍で伸びしろあり)。なお database = 'datasets' で絞っているのは、外すと system.* などの内部テーブルまでランキングに入ってくるためです。本番で全テーブルを横断して見たいときは、この条件を外す(または system だけ除外する)と良いです。また system.parts が対象にするのは MergeTree 系テーブルなので、すべてのエンジンのディスク使用量が出るわけではない点も頭に入れておきます。
断片化は次のSQLでチェックできます。
SELECT database, table, count() AS active_parts
FROM system.parts
WHERE active
GROUP BY database, table
HAVING active_parts > 100
ORDER BY active_parts DESC
結果はこのとおりです。
| database | table | active_parts |
|---|---|---|
| datasets | events_raw | 150 |
狙いどおり events_raw だけが引っかかりました。アクティブパーツが数百を超えているテーブルは、細かいバッチで INSERT しすぎ(マージが追いついていない)のサインです。Claude に「なぜパーツが増えるのか、対策は?」と続けて聞くと、INSERT のバッチサイズを大きくする・OPTIMIZE の検討、といった方向まで説明してくれます。
ひとつ補足しておくと、パーツは同じパーティション内でしかマージされません。日付などでパーティションを切っているテーブルでは、テーブル全体の合計ではなく partition_id ごとのパーツ数を見たほうが正確です(GROUP BY database, table, partition_id にする)。「100個」も絶対的な基準ではなく、あくまで経験則の目安として捉えてください。
健康診断その3:エラーの傾向
最後に「最近どんなエラーが出ているか」です。system.errors はサーバー起動以来のエラーを種類ごとに集計してくれている、地味だけど便利なテーブルです。
> 件数の多いエラーTOP10を、件数と最後に出た時刻つきで。
SELECT
name,
value AS count,
last_error_time,
last_error_message
FROM system.errors
WHERE value > 0
ORDER BY value DESC
LIMIT 10
実行すると、こんな顔ぶれが出てきました。
| name | count | last_error_time | last_error_message |
|---|---|---|---|
| UNKNOWN_DATABASE | 15 | 2026-06-20 16:43:31 | Database claude_memory does not exist |
| MEMORY_LIMIT_EXCEEDED | 6 | 2026-06-20 16:43:11 | Query memory limit exceeded: would use 48.93 MiB ... |
| UNKNOWN_TABLE | 3 | 2026-06-20 16:43:11 | Unknown table expression identifier 'datasets.nonexistent_table' ... |
MEMORY_LIMIT_EXCEEDED(メモリ上限超え)と UNKNOWN_TABLE(存在しないテーブル)は、今回わざと仕込んだエラーです。一方の UNKNOWN_DATABASE(claude_memory という別ツールが参照した存在しないDB)は、こちらが意図していないのに勝手に拾えています。system.errors はサーバーに飛んできた失敗を種類ごとに全部数えているので、自分が把握していないアクセスのエラーまで炙り出せるのが便利なところです。
「いつ・どのクエリで」まで追いたいときは system.query_log の例外行を時間帯で見ます。
SELECT
toStartOfHour(event_time) AS hour,
count() AS exceptions,
any(exception) AS sample
FROM system.query_log
WHERE type IN ('ExceptionWhileProcessing', 'ExceptionBeforeStart')
AND event_time > now() - INTERVAL 1 DAY
GROUP BY hour
ORDER BY hour DESC
MEMORY_LIMIT_EXCEEDED が特定の時間帯に固まっているなら、バッチ処理とユーザークエリがぶつかっている、といった仮説が立ちます。ここまで来ると、もう普通にインシデント調査です。
もう一歩:毎朝の健康診断をSlackに流す
ここまでは対話でしたが、claude -p(ワンショット実行)を使えば定期実行にもできます。launchd や cron で毎朝叩いて、健康診断レポートを Slack に投げる、という運用です。
#!/usr/bin/env bash
# daily_ch_healthcheck.sh
REPORT=$(claude -p "ClickHouseの健康診断をして。
直近24時間の重いクエリTOP3、パーツが100を超えてるテーブル、
件数の多いエラーTOP3を、簡潔な日本語のレポートにまとめて。" \
--allowedTools "mcp__clickhouse__run_query")
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg t "$REPORT" '{text: $t}')"
--allowedTools で使えるツールを run_query だけに絞っておくと、自動実行でも余計なことをされません。ヘッドレスの claude -p では、承認を求める人がいないので、許可リストに無いツールの呼び出しはそのまま拒否されます。だからこのリストが実質的なホワイトリストとして効きます。
ひとつ注意点として、claude mcp add は既定だと実行したディレクトリに紐づく local スコープでMCPを登録します。launchd や cron は別のディレクトリから起動することが多く、そのままだと登録したMCPが見つからないことがあります。定期実行で使うMCPは claude mcp add --scope user ... で登録して、どのディレクトリからでも見えるようにしておきます。
これを launchd で毎朝7時に回せば、出社前に ClickHouse の状態がSlackに届きます。
ハマりどころ
実際にやってみて引っかかりやすい点をまとめておきます。
-
query_logが空:log_queries=1になっているか、立てたばかりで履歴が無いだけ、のどちらか。何回かクエリを流せば溜まります -
書き込み事故:DB側
readonly=1+ MCP側の読み取り専用デフォルトの二重ガードを必ず。自動実行では--allowedToolsでツールも絞る -
QueryFinishフィルタ忘れ:query_logは1クエリ複数行。集計が倍に見えたらこれを疑う -
activeフィルタ忘れ:system.partsは非アクティブパーツも含む。ディスク量・行数を過大に見積もる原因 -
巨大テーブルへの不用意なクエリ:診断対象が
system.*とはいえ、ついでにSELECT * FROM 巨大テーブルを投げさせないよう、プロンプトで「systemテーブルのみ見て」と釘を刺すと安全 -
Cloud と複数ノードの差:
system.query_logやsystem.partsはノードごとに別々なので、ClickHouse Cloud や複数ノード構成ではclusterAllReplicas(default, system.query_log)のように全ノードを横断して見る必要がある。分散クエリは1リクエストが親(is_initial_query = 1)と子に分かれて記録されるので、件数を数えるときはここも意識する。単一ノードの Docker 環境では気にしなくてよい
まとめ
ClickHouse の MCP サーバーは「自然言語で集計」だけのものと思われがちですが、system.* を読ませると運用診断エージェントとして化けます。
- 調べる対象(
query_log/parts/errors)が決まっているので、エージェントが迷子になりにくい - 「仮説 → 確認クエリ → 次の仮説」という人間の調査手順を、ツールループがそのまま代行する
- 読み取り専用で繋げば、運用中のDBにも比較的安心して向けられる
「分析」より一段地味ですが、毎日DBの面倒を見ている人ほど効きます。まずはローカルの Docker で立てて、system.query_log に話しかけてみてください。