Claude Codeでautovacuumとテーブル肥大を調べる手順 — 閾値調整とpg_repackの判断
pg_stat_user_tablesのn_dead_tupとlast_autovacuumをClaude Codeに渡し、テーブル別のALTER TABLE案とpg_repackの要否を出させる手順です。変更は検証用DBで試します。
PostgreSQLのテーブルが肥大するとき、まず見るのは「autovacuumが走っていないのか、走っているのに追いついていないのか、過去の肥大がそのまま残っているのか」の3択です。pg_stat_user_tablesの数列をClaude Codeに渡すと、この3択をテーブルごとに振り分け、ALTER TABLEの案とpg_repackの要否まで下書きさせられます。
ここでの役割分担は決まっています。Claude Codeは統計を読んで案を出し、変更を流すのは検証用のDBまで。本番への反映は人が判断します。この記事では、渡す数列、閾値の計算、案の形、pg_repackに進む条件を順に並べます。
先に押さえる事実: autovacuumが回収するもの、しないもの
調べる前に、PostgreSQLの仕様を3つ確認しておきます。Claudeに渡すプロンプトの前提にもなります。
標準のVACUUM(autovacuumも同じ)は、テーブルとインデックスの死んだ行バージョンを取り除き、その領域を再利用できる状態にします。ただしディスクをOSへ返すのは、テーブル末尾のページが丸ごと空き、排他ロックも簡単に取れる特別な場合に限られます。ファイルを小さくしたいなら、別の手段が要ります。
VACUUM FULLはテーブルを新しいファイルに書き直して空き領域を詰めますが、ACCESS EXCLUSIVEロックが要り、書き直しの間は他の操作と並行できません。コピー分の追加ディスクも必要です。autovacuum自身はVACUUM FULLを発行しません。
つまり、肥大の対処は2層に分かれます。
肥大対策の2層
再発させない
死んだ行が溜まる前に標準VACUUMを回す。ALTER TABLEでテーブル別に調整する。
すでに膨らんだ分を戻す
VACUUM FULL、CLUSTER、pg_repackなど。ロックと追加ディスクのコストを払う。
閾値を直さないまま書き直すと、また同じ速度で膨らみます。逆に、閾値だけ直しても過去の肥大は縮みません。Claudeには「どちらの層の話か」を、テーブルごとに判定させます。
Claude Codeに渡す前の準備
変更を流せないようにしておく
統計の読み取りと、設定変更や書き直しは別の権限にします。本番に向く接続は、書き込み権限のないロールで作ります。ロールの作り方はClaudeでスロークエリをEXPLAIN ANALYZEで絞る記事の統計専用ロールと同じ考え方で、ここでは繰り返しません。
Claude Code側では、.claude/settings.jsonで実行できるコマンドを絞ります。権限ルールの書式は、Claude Codeのベストプラクティスにある例と同じ形です。
{
"permissions": {
"allow": ["Bash(psql --version)"],
"ask": ["Bash(psql *)"],
"deny": ["Bash(pg_repack *)", "Read(./.env)", "Read(./.env.local)"]
}
}psql *をaskに置くと、クエリを流すたびに人が確認します。pg_repackはdenyにして、Claudeからは実行させません。denyのルールは権限モードにかかわらず効くと説明されているため、「書き直しは人だけ」という線をここで引けます。
なお、ルールが照合するのはツール呼び出しであって意図ではありません。スクリプトの中から呼ばれるコマンドまでは止まらないので、スクリプトを書かせる場合は中身を読んでから流してください。
CLAUDE.mdに書くのは、コードから読めないことだけ
Claude Codeのベストプラクティスは、CLAUDE.mdには「コードを読んでも見つからないこと」だけを書くよう勧めています。今回なら、DBの接続先の使い分けと、変更の禁止線がそれにあたります。
# DB調査の約束
- 調査は検証用DB(接続名: stg)で行う。本番(prod)は読み取り用ロールのみ
- ALTER TABLE / VACUUM FULL / pg_repack は実行しない。SQLを提案するだけにする
- 提案には必ず根拠のクエリ結果(列名と値)を付ける
- 閾値の計算式は PostgreSQL の公式ドキュメントのとおりに書く最後の1行は、閾値をモデルの記憶で計算させないための保険です。次節の式をそのまま使わせます。
渡す数列: pg_stat_user_tablesとpg_class
pg_stat_user_tablesはpg_stat_all_tablesと同じ列を持ち、ユーザーテーブルだけに絞ったビューです。肥大の調査で使う列は次のとおりです。
| 列 | 意味 |
|---|---|
n_live_tup | 意味生きている行数の推定値 |
n_dead_tup | 意味死んだ行数の推定値 |
last_autovacuum | 意味autovacuumが最後にVACUUMした時刻 |
autovacuum_count | 意味autovacuumがVACUUMした回数 |
n_mod_since_analyze | 意味最後のANALYZE以降に変更された行数の推定値 |
どれも「推定値」です。n_dead_tupが大きいことは、肥大している可能性を示すだけで、ファイルの実サイズとは別の話です。そこで、サイズと個別設定も一緒に取ります。
SELECT s.relname,
s.n_live_tup, s.n_dead_tup,
round(100.0 * s.n_dead_tup
/ nullif(s.n_live_tup + s.n_dead_tup, 0), 1) AS dead_pct,
s.last_autovacuum, s.autovacuum_count,
s.n_mod_since_analyze,
c.reltuples::bigint AS reltuples,
c.reloptions,
pg_size_pretty(pg_total_relation_size(s.relid)) AS total_size,
(current_setting('autovacuum_vacuum_threshold')::float8
+ current_setting('autovacuum_vacuum_scale_factor')::float8
* c.reltuples)::bigint AS trigger_dead_tup
FROM pg_stat_user_tables s
JOIN pg_class c ON c.oid = s.relid
ORDER BY s.n_dead_tup DESC
LIMIT 20;最後のtrigger_dead_tupは、全体設定だけで計算した「autovacuumが動き出す死んだ行数」です。reloptionsが空でない行は、そのテーブルに個別の設定があるので、その値で読み替えます。
この式の根拠は、次の閾値の定義です。
vacuum threshold = Minimum(vacuum max threshold,
vacuum base threshold + vacuum scale factor * number of tuples)基本の閾値はautovacuum_vacuum_thresholdで既定は50行、割合はautovacuum_vacuum_scale_factorで既定は0.2(テーブルの20%)です。上限のautovacuum_vacuum_max_thresholdは既定で1億行です。たとえば1000万行のテーブルなら、既定値のままでは約200万行が死ぬまでautovacuumは動きません。式どおりに50 + 0.2 × 10,000,000を計算した値で、実例の数字ではなく計算例です。
この結果をClaudeに貼るとき、別のコマンドで取った数字を混ぜないことがコツです。後で根拠を追えなくなります。
Claudeに出させる案: 3択で振り分ける
プロンプトでは、出力の形を先に決めます。「テーブルごとに、次のどれかに分類し、根拠の列と値を引用する」という指示です。
貼ったクエリ結果を、テーブルごとに A〜D のどれかに分類してください。
A: autovacuum が追いついていない(閾値が高すぎる)
B: autovacuum は走っているが n_dead_tup が減らない
C: n_dead_tup は低いがサイズが大きい(過去の肥大の可能性)
D: 問題なし
各テーブルに、根拠の列名と値を必ず付けてください。
A は ALTER TABLE の案と、変更後に autovacuum が動き出す死んだ行数を
式で計算してください。C は pg_repack の要否を判断する材料を列挙してください。
SQL は実行せず、提案として出してください。分類の読み方の目安は次のとおりです。Claudeの出力をレビューするときの物差しとして使えます。
| 分類 | 見える姿 | 疑うこと |
|---|---|---|
| A | 見える姿n_dead_tupがtrigger_dead_tupに届かず、last_autovacuumが古いか空 | 疑うこと閾値が大きすぎる |
| B | 見える姿last_autovacuumは新しくautovacuum_countも多いのに、n_dead_tupが高い | 疑うこと回収を妨げる要因がある |
| C | 見える姿dead_pctは低いがtotal_sizeが大きい | 疑うこと過去に膨らんだ分が残っている |
| D | 見える姿dead_pctが低く、サイズも想定内 | 疑うこと対応不要 |
Bは判断が難しい型です。PostgreSQLのドキュメントでは、トランザクションIDの周回対策の手順として、長く開いたままのトランザクションをpg_stat_activityで探して閉じることと、古いレプリケーションスロットをpg_replication_slotsで探すことが挙げられています。Bのテーブルがあれば、同じ観点で確認する候補になります。ロック待ちが絡む調査はデッドロックの原因を調べる記事と同じ手順で進められます。
autovacuumの動きには、別の制約もあります。
- 一時テーブルにはautovacuumが触れないため、セッションからSQLで実行する必要がある
- パーティションテーブルの親は直接処理されない。パーティション自体は他のテーブルと同じように処理される
- SHARE UPDATE EXCLUSIVEロックと衝突するコマンド(ANALYZEなど)を定期的に流すと、autovacuumが完了できなくなる場合がある
Claudeの案に「一時テーブル」「親テーブル」が含まれていたら、この制約に当たっていないかを見ます。
ALTER TABLE案を検証用DBで試す
Aと判定されたテーブルには、テーブル別の閾値を設定します。割合を下げ、基本の閾値を上げる形が典型です。
-- 例: 1000万行のテーブルで、約10万行の死んだ行で動かす
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 1000
);この場合の動き出しは1000 + 0.01 × 10,000,000 = 101,000行です。数値は例で、テーブルの更新頻度とVACUUMにかかる時間を見て決めます。
ALTER TABLE ... SETにはいくつか押さえるべき点があります。
- autovacuum系のストレージパラメータの変更は、
SHARE UPDATE EXCLUSIVEロックを取る - コマンドはテーブルの中身をすぐには書き換えない。テーブル別の値は、autovacuumがそのテーブルを処理するときに全体設定より優先して使われる
- 元に戻すときは
RESETを使う
ALTER TABLE orders RESET (
autovacuum_vacuum_scale_factor,
autovacuum_vacuum_threshold
);Claudeには、SETと対になるRESETを同時に出させます。戻し方のない変更案は、レビューが止まる原因になります。
検証の閉じ方は、Claude Codeのベストプラクティスにある「結果が合否で返るチェックを持たせる」考え方に沿います。変更後に同じクエリをもう一度流し、次を比べさせます。
autovacuum_countが増え、last_autovacuumが進んだかn_dead_tupが、変更前より低い水準で上下しているかreloptionsに設定が入っているか
「変更しました」の報告ではなく、コマンドと出力を根拠として出させます。出力を読む作業は、自分でクエリを再実行するより速く終わります。
落とし穴
割合を下げすぎて、ワーカーが足りなくなる
閾値を下げたテーブルが増えると、autovacuumの同時実行数(autovacuum_max_workers、既定3)を使い切ります。大きなテーブルが長くVACUUMを続けると、他のテーブルが順番待ちになります。個別設定は、Aに該当した少数のテーブルに絞ってください。
I/Oへの影響を抑える仕組みもあります。autovacuumはautovacuum_vacuum_cost_delay(既定2ミリ秒)で自分に待ちを入れており、複数のワーカーが動くときはその制限が全体で按分されます。テーブル別にautovacuum_vacuum_cost_delayやautovacuum_vacuum_cost_limitを設定すると、そのテーブルを処理するワーカーは按分の対象から外れます。Bの原因がVACUUMの遅さなら、ここが調整点になりますが、I/Oへの影響が増えるため、検証用DBで負荷を見てから決めます。
pg_repackが要るかを判断する
Cに分類したテーブル、またはAの閾値を直しても縮まないテーブルが、書き直しの候補です。Claudeには「実行する」のではなく「判断材料を表にする」ことを求めます。
| 比べる点 | VACUUM FULL | pg_repack |
|---|---|---|
| ロック | VACUUM FULL書き直しの間ずっとACCESS EXCLUSIVE | pg_repack準備と最後の入れ替えの短い間だけACCESS EXCLUSIVE |
| 追加ディスク | VACUUM FULL新しいコピー分が必要 | pg_repack対象テーブルとインデックスの合計の約2倍の空き |
| 対象の条件 | VACUUM FULLなし | pg_repackPRIMARY KEY、またはNOT NULL列のUNIQUEインデックスが必要 |
| 実行中のDDL | VACUUM FULL実行中は不可(排他ロックのため) | pg_repack対象テーブルへのDDLは不可 |
pg_repackは、変更を記録するログテーブルとトリガーを作り、新しいテーブルに行をコピーしてから、テーブルを入れ替える仕組みです。途中のINSERT・UPDATE・DELETEは通常どおり進みます。一時テーブルと、GiSTインデックスでのクラスタ化は対象外です。
判断の流れは、Claudeにこの順で確認させます。
書き直しの要否を詰める順番
- 1
サイズを確認する
total_sizeから、書き直しで空く見込みの量と、必要な空きディスクを見積もる。 - 2
テーブルの条件を確かめる
PRIMARY KEYまたはNOT NULLのUNIQUEインデックスがあるか。無ければ
pg_repackは使えない。 - 3
再発しない状態にする
先にAの閾値調整を済ませる。そうしないと、書き直しても再び膨らむ。
- 4
何が実行されるかを確認する
pg_repackの--dry-runで、対象を列挙させる。 - 5
実行は人が決める
実行時間帯、ロックの影響、空きディスクを人が確認して実行する。
手順4の確認は次のコマンドです。
pg_repack --dry-run --table=orders --dbname=stg--dry-runは「何を書き直すかを一覧にして終了する」オプションです。本番へ向ける前に、検証用DBで実行時間とディスクの増え方を測っておくと、Claudeの見積もりとの差が分かります。
書き直しを急ぐ理由が薄いときは、そもそも実行しない選択もあります。ドキュメントは、標準のVACUUMを適度な頻度で回して領域を定常的に使う運用を、頻度の低いVACUUM FULLより勧めています。ファイルが大きいままでも、空きが再利用されていれば、その領域は次の更新で埋まるからです。
マネージドDBで使うときの足し算
接続先がCloud SQLやAlloyDBなら、Claude Code側の導線を足せます。Cloud SQL for PostgreSQLプラグインの記事の接続でクエリを流す場合も、権限は同じように絞ります。pg_repackの拡張が使えるかは、サービス側の対応状況を確認してください。Claude側の手順は変わりませんが、pg_repackの前提が崩れることがあります。
MCPサーバーで統計やヘルスチェックを読ませたい場合は、Postgres MCP Proの記事にある構成が使えます。
よくある質問
n_dead_tupが0なのに、テーブルが大きいままなのはなぜですか。
標準のVACUUMは、領域を再利用できる状態にするだけで、OSへ返すのは末尾のページが空いた場合に限られます。n_dead_tupが低くサイズが大きいテーブルは、前述のCにあたります。縮めたいときだけ、書き直しを検討します。
autovacuumを止めたほうが負荷は下がりますか。
ドキュメントは、負荷がきわめて予測しやすい場合を除いて、デーモンを完全に止めるのは賢明でないとしています。止めても、トランザクションIDの周回を防ぐためのautovacuumは起動します。閾値や待ちを調整する方向が、負荷対策の通常の入口です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。