データベース

PostgreSQLのVACUUM運用|autovacuumのしきい値設計とXID周回・肥大の切り分け

PostgreSQLのVACUUM運用|autovacuumのしきい値設計とXID周回・肥大の切り分け

PostgreSQLの運用でVACUUMが表に出てくるのは、たいてい良くない場面です。ディスク使用量が想定の三倍に膨らんでいる、同じ件数を返すクエリが半年前の五倍遅い、あるいはログに「must be vacuumed within」という見慣れない警告が出ている。いずれもVACUUMが追いついていないという一つの現象の別の顔で、原因は設定値と実データの規模が噛み合っていないところにあります。

この記事は、PostgreSQL固有の周期処理としてのVACUUMを扱う解説です。不要タプルが生まれる構造、autovacuumが起動するしきい値の数式と既定値、凍結とトランザクションID周回、そして肥大を検知して原因を切り分ける手順までを、実行できるSQL付きで並べます。トランザクションそのものの挙動はトランザクションとは?ACID・分離レベル・commitとrollbackを実装目線で解説に、データベース全般の位置づけはデータベースとは?種類・DBMS・RDBとNoSQLの選び方に譲ります。数値の前提は18系(2026年8月時点で18.6が最新マイナー)としました。

まとめ:VACUUM運用で先に決める4点

先に結論を4点で示します。第一に、VACUUMは「消えたはずの行の跡地」を掃除する処理ではなく、追記型の更新方式が構造的に生む不要タプルを回収可能な空き領域に戻す処理です。UPDATEは行を書き換えず、新しい版を追加して古い版を無効にします。したがって更新の多いテーブルほど、削除を一度もしていなくても不要タプルは増えていきます。

第二に、autovacuumが起動する境目は行数に比例します。既定は「50+0.2×行数」で、1,000万行のテーブルなら約200万件の不要タプルが溜まるまで動きません。18からはこの式が上限値との最小値を取る形になり、5億行を超えるあたりで打ち止めが効き始めます。裏を返せば、それ未満の大テーブルは今も個別設定で比率を下げるしかありません。

第三に、凍結は四つの年齢設定で段階的に強まります。5,000万で凍結の対象になり、1.5億で全ページ走査へ切り替わり、2億で自動起動が強制され、16億では安全装置によって索引処理まで捨てる段階です。周回まで残り4,000万で警告、残り300万で書き込みが止まるという公式の具体値を知っていれば、監視のしきい値を勘で置かずに済みます。

第四に、VACUUMを流したのに不要タプルが減らないときは、設定ではなく可視性の地平が止まっています。長時間の未終了トランザクション、消費されていないレプリケーションスロット、放置されたプリペアドトランザクション、スタンバイからのフィードバックの四つが典型で、いずれも実行中のSQLひとつで犯人を特定できます。以下、順に根拠を見ていきましょう。

追記型アーキテクチャが不要タプルを残す仕組みと肥大が生む負荷

PostgreSQLは行を上書きしません。UPDATEは同じテーブル内に新しい版のタプルを書き、古い版には「このトランザクション以降は見えない」という印を付けるだけです。DELETEも同様に、実体は残したまま無効の印を付けます。この方式のおかげで読み手は書き手を待たずに一貫した断面を読めますが、代償として無効なタプルが物理ファイルの中に残り続けます。

UPDATEが行を書き換えず新しい版を追記していく追記型の構造

実際に見てみると挙動は明快です。10万行のテーブルを一度だけ全件更新すると、生きている行は10万のままで、無効になった行が10万件そのまま計上されます。

CREATE TABLE t (id int PRIMARY KEY, v text);
INSERT INTO t SELECT g, 'x' FROM generate_series(1,100000) g;
UPDATE t SET v = 'y';

SELECT n_live_tup, n_dead_tup FROM pg_stat_user_tables
 WHERE relname = 't';

ここで返る n_dead_tup が不要タプルの概数です。同じ更新を五回繰り返せば、行数は変わらないのにテーブルの物理サイズは六倍近くまで伸びます。追記型の更新を多用する設計、たとえば競合時に更新へ倒す書き方についてはPostgreSQLのUPSERT実装|ON CONFLICTの競合ターゲット指定とMERGE文の使い分けで扱いました。競合が起きた分だけ古い版が積み上がる点は、設計段階から見込んでおくほうが安全でしょう。

不要タプルが容量と索引の走査量という二方向で効いてくる理由と監視方法

不要タプルの害は二方向です。ひとつは単純な容量で、テーブルと索引の両方が膨らむ構造です。もうひとつが見落とされやすい走査量で、シーケンシャルスキャンは無効なタプルの載ったページも読み込むため、有効な行が同じでも入出力の量が増えます。索引側も削除済みのエントリを抱えたまま深くなり、検索の到達コストが上がります。

統計情報の劣化も並行して起きます。プランナが参照する行数の見積もりが実態からずれると、索引スキャンを選ぶべき場面で全走査へ倒れることがあります。倒れているかどうかの判定はPostgreSQLの実行計画の読み方|EXPLAIN ANALYZEと見積もり乖離の診断で扱う見積もり行数と実測行数の比較で切り分けられます。VACUUMとANALYZEが別の処理でありながら自動実行では同じデーモンが担うのは、この二つが同じ原因から同時に悪化するためです。

VACUUMが空き領域を再利用可能に戻す範囲とOSへ返す範囲

標準のVACUUMは、不要タプルの領域をそのテーブル内で再利用できる状態に戻します。ファイルサイズ自体は基本的に縮みません。例外はテーブル末尾に連続した空きページができた場合で、このときだけファイルを切り詰めてOSへ返します。18では切り詰めの可否をサーバー変数 vacuum_truncate でも指定できるようになり、既定は有効です。

この性質は運用の期待値を左右します。日次のVACUUMでディスク使用量が減らないのは異常ではなく、設計どおりの動作でした。逆に言えば、いったん膨らんだファイルを縮めたいなら別の手段が要ります。標準のVACUUMが取るロックは共有更新排他で、SELECTもINSERTもUPDATEも並行して動き続けます。止まるのはALTER TABLEのような定義変更だけです。

autovacuumの起動条件を決める数式と18で変わった打ち止め

自動実行の判断は、テーブルごとに次の比較で決まります。18の公式ドキュメントに載っている形はこうです。不要タプルの数が「上限値」と「基礎しきい値+規模係数×行数」の小さいほうを超えたら起動する、という最小値を取る式になっています。

削除と更新に反応するしきい値の既定値と5億行で効く上限の算定方法

既定値は autovacuum_vacuum_threshold が50件、autovacuum_vacuum_scale_factor が0.2です。ここまでは以前の版と同じですが、18で autovacuum_vacuum_max_threshold が加わり、既定で1億件の打ち止めが入りました。0.2×行数が1億に達するのは行数が5億のときなので、5億行を超える巨大テーブルでは上限側が効き始めます。

裏返すと、数千万行から一億行程度のテーブルは18でも比率のままです。1,000万行なら約200万件、5,000万行なら約1,000万件が溜まるまで自動実行は動きません。この待ち時間の間に肥大が進むので、大きなテーブルだけ個別に比率を下げる運用は18でも変わらず有効でしょう。分析用の統計取得は autovacuum_analyze_threshold が50件、規模係数が0.1で、こちらは更新側より二倍細かく反応します。

追記専用テーブルに効く挿入側のしきい値と凍結率による補正の判断基準

ログや履歴のように挿入しかしないテーブルは、不要タプルが増えないため上の式では永遠に起動しません。この穴を埋めるのが挿入側のしきい値で、autovacuum_vacuum_insert_threshold の既定が1,000件、autovacuum_vacuum_insert_scale_factor が0.2です。18ではこの式に凍結済みページの比率による補正が入り、すでに凍結が済んでいる分だけ発動が緩やかになりました。

挿入側の判定が入った理由は、可視性マップと凍結を前倒しで進めるためです。追記専用テーブルを放置すると、周回対策の強制VACUUMが数億トランザクション後にまとめて走り、そこで初めて長時間の全走査が発生します。前倒しで少しずつ処理しておくほうが、運用時間帯への影響を読みやすくなります。

大きなテーブルだけ比率を下げるテーブル単位の個別設定の決め方

設定はテーブル単位で上書きできます。全体の既定を下げるとカタログや小テーブルまで頻繁に走ってしまうため、対象を絞るのが定石です。

ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.02,
  autovacuum_vacuum_threshold    = 5000,
  autovacuum_vacuum_cost_delay   = 2
);

SELECT reloptions FROM pg_class WHERE relname = 'orders';

比率0.02なら1,000万行で約20万件が境目になり、一回あたりの処理量が小さく分散します。並行して見ておきたいのが同時実行数で、autovacuum_max_workers の既定は3、稼働間隔の autovacuum_naptime は1分です。18では autovacuum_worker_slots が加わり、既定16の枠内であれば再起動なしで作業プロセス数を増やせるようになりました。マネージド環境では設定の入口が変わるので、その差はAmazon RDS for PostgreSQLとは?マルチAZ構成・拡張機能の統制とDMS移行やAurora PostgreSQLとは?対応バージョン・拡張機能とRDSからの移行判断で触れたパラメータグループの扱いを併せて確認してください。

VACUUM FULLと凍結の使い分け・トランザクションID周回の防ぎ方

標準のVACUUMで足りない場面は二つあります。物理サイズを実際に縮めたい場面と、周回を避けるために古い行を凍結する場面です。前者がVACUUM FULL、後者がFREEZEで、性格はまったく異なります。

標準のVACUUMとVACUUM FULLのロック水準と必要ディスク量

VACUUM FULLはテーブルを新しいファイルへ書き直す処理です。結果として無駄のない状態に詰め直されOSへ領域が返りますが、取るロックはアクセス排他で、読み取りも含めて全ての操作が待機状態になります。さらに、完了までは新旧二つの実体が並存するため、そのテーブルとほぼ同じ容量の空きディスクが必要です。公式ドキュメントも標準のVACUUMを使いFULLは避けるよう勧めています。

採用条件は絞り込めます。すでに肥大が数十パーセントに達していて標準のVACUUMでは回収しきれない、かつ計画停止の枠が取れる、かつテーブルサイズと同等の空きがある。この三つが揃ったときだけです。揃わないなら、オンラインで詰め直す外部拡張を検討するか、パーティションを切って古い区画ごと落とす設計へ寄せるほうが現実的でしょう。

凍結が四段階で強まる仕組みと年齢設定の既定値の一覧と監視基準

PostgreSQLのトランザクションIDは32ビットで、およそ42億の空間を循環して使います。あるトランザクションから見て「古い側」と「新しい側」はそれぞれ約20億ずつで、この境目を越えると過去の行が未来の行に見える事態です。これを防ぐのが凍結で、十分に古い行へ特別な印を付けて年齢の比較対象から外します。

設定 既定値 その年齢で起きること
vacuum_freeze_min_age 5,000万 この年齢の行を凍結する
vacuum_freeze_table_age 1.5億 全ページ走査へ切り替え
autovacuum_freeze_max_age 2億 自動実行を強制起動する
vacuum_failsafe_age 16億 安全装置が働き簡略化

安全装置が働くと、待ち時間の挿入が止まり、索引の掃除といった必須でない処理は飛ばされ、バッファの使用制限も外れます。周回の回避だけに全力を注ぐ状態です。ここまで来ている時点で運用は破綻しているので、監視の目標は2億を大きく下回る水準に置くべきでしょう。マルチトランザクションIDにも同じ構造の設定があり、autovacuum_multixact_freeze_max_age の既定は4億です。18では通常のVACUUMも一部のページを前倒しで凍結するようになり、その積極度は vacuum_max_eager_freeze_failure_rate(既定0.03)で調整できます。

周回まで残り4,000万で警告・残り300万で書き込みが止まる線

放置した場合の挙動も具体値まで決まった仕様です。周回まで4,000万トランザクションを切るとログに「must be vacuumed within」という警告が出続け、残り300万で新しいトランザクションIDの払い出しが拒否されます。この状態になると書き込みは一切通らず、読み取り専用のトランザクションだけが動きます。

SELECT datname,
       age(datfrozenxid) AS xid_age,
       2147483647 - age(datfrozenxid) AS remaining
  FROM pg_database
 ORDER BY xid_age DESC;

この remaining が減り続けているなら、自動実行が何かに阻まれています。復旧はシングルユーザーモードでの実行が要る場合もあり、対応時間は数時間単位になります。監視項目としてxid_ageを常時取得し、2億の手前で通知が飛ぶようにしておくのが最小限の備えです。

肥大の検知とVACUUMを流しても減らないときの原因の切り分け

設定を整えても不要タプルが減らないことがあります。このときに設定値を触り続けても状況は変わりません。見るべきは、どこまで古い行なら消してよいかを決める可視性の地平です。

pg_stat_user_tablesで肥大を測る指標と18で増えた所要時間の列

最初の一歩は統計ビューの確認です。カタログの参照方法そのものはPostgreSQLのテーブル一覧を取得する方法|メタコマンドとカタログの使い分けで整理しました。

SELECT relname, n_live_tup, n_dead_tup,
       n_ins_since_vacuum, last_autovacuum,
       total_autovacuum_time
  FROM pg_stat_user_tables
 ORDER BY n_dead_tup DESC
 LIMIT 20;

末尾の total_autovacuum_time は18で加わった列で、total_vacuum_time、total_analyze_time、total_autoanalyze_time と併せて累計の所要時間が取れます。従来は「いつ走ったか」しか分からず、時間がかかっているのか回数が足りないのかを切り分けられませんでした。より正確な肥大率が要るなら pgstattuple 拡張の dead_tuple_percent や approx_free_percent を使いますが、全走査に近い負荷がかかるため対象と時間帯を絞ってください。

可視性の地平を止める四つの原因と犯人を特定する実行SQLの読み方

不要タプルを消してよいかどうかは、動作中の最も古いトランザクションが決めます。それより新しい版は誰かが見ているかもしれないため回収できません。この地平を止める犯人は四つに分かれます。

SELECT pid, state, backend_xmin, xact_start
  FROM pg_stat_activity
 WHERE backend_xmin IS NOT NULL
 ORDER BY age(backend_xmin) DESC;

SELECT slot_name, active, xmin, catalog_xmin
  FROM pg_replication_slots;

SELECT gid, prepared FROM pg_prepared_xacts;

一つめは長時間の未終了トランザクションで、アプリがBEGINしたまま放置している状態が典型です。夜間のpg_dumpが長引いた場合も同じ状態になるため、取得の時間帯と並列度の決め方はPostgreSQLのバックアップ設計|pg_dumpとPITRの使い分け・復旧検証の手順で扱っています。二つめは消費されていないレプリケーションスロットで、停止したスタンバイの分が残っているとその時点の地平が固定されます。三つめは二相コミットの残骸、四つめはスタンバイからのフィードバックを有効にした構成でした。レプリケーション側の設計判断はデータベースレプリケーションとは?同期・非同期の違いとレプリカ遅延の設計で扱っており、遅延の許容とVACUUMの回収可能範囲はここで裏表になります。

処理時間が伸びるときに見るメモリと待ち時間の設定の版差の確認方法

一回のVACUUMが長すぎる場合は、メモリと待ち時間を見ます。作業メモリは maintenance_work_mem(既定64MB)で、これが小さいと不要タプルのIDを抱えきれず索引を何度も往復します。17でこの領域の管理方式が変わり、それまで暗黙に効いていた1GBの上限が撤廃されました。大きなテーブルを抱える環境では、17以降へ上げたうえで数GBを割り当てる価値があります。

待ち時間の側は自動実行の autovacuum_vacuum_cost_delay(既定2ミリ秒)と vacuum_cost_limit(既定200)で決まり、ページ参照のコストは命中1・読み込み2・更新20と数えます。処理を急がせたいなら制限値を上げ、本番の負荷を守りたいなら待ち時間を延ばす、という逆方向の調整です。なお進捗ビュー pg_stat_progress_vacuum は17で列名が変わり、旧来の max_dead_tuples などは max_dead_tuple_bytes や num_dead_item_ids に置き換わりました。16以前向けの監視スクリプトをそのまま持ち込むと壊れる箇所なので、版上げの前に確認してください。

自前で回すか保守運用に載せるかを判断する三つの運用条件と選定基準

ここまでの内容は、設定の当て方が分かれば内製でも回せます。境目になるのは、担当者が張り付ける時間があるかどうかです。判断材料は三つに整理できます。

第一に、24時間の監視が要るかどうか。周回の警告や地平の固着は深夜帯に進行し、朝の出社時には手遅れという形で顕在化します。第二に、対象テーブルの規模と本数。数億行のテーブルが複数あり、パーティション設計や再編成まで含めて判断が要るなら、都度の設計工数が無視できません。第三に、版上げの計画があるかどうかです。17以降のメモリ管理や18の新設定は効き方が変わるため、上げるタイミングで設定の棚卸しが発生します。

三つのうち二つ以上が当てはまるなら、監視と是正を含む保守の枠に載せるほうが総コストは下がります。当社ではシステム保守運用・内製化支援として、24時間の監視から設定の見直し、最終的に運用を社内へ戻すところまでを扱っています。逆に一つ以下なら、この記事の監視SQLを日次のジョブに組み込み、しきい値の超過だけ通知する形で十分に回るはずです。

よくある質問

autovacuumを止めて夜間の定時VACUUMだけにしてよいですか?

勧めません。自動実行は不要タプルの回収だけでなく周回対策の強制起動も担っており、止めると気付かないうちに年齢が積み上がります。夜間の負荷が問題なら、止めるのではなく待ち時間を延ばして日中の処理量を絞る調整のほうが安全でしょう。定時実行は自動実行の補助として併用する位置づけになります。

VACUUM FULLの代わりに使える無停止の方法はありますか?

テーブルをオンラインで詰め直す外部拡張があり、一時的に同容量の作業領域を使いながら短時間の排他ロックだけで置き換えます。マネージド環境では利用の可否が提供側の対応拡張一覧に依存するため、事前確認が要ります。将来にわたって肥大が読めるテーブルなら、日付でパーティションを切って古い区画を切り離す設計に変えるほうが根本的です。

n_dead_tupが0なのにテーブルが大きいままなのはなぜですか?

回収は済んでいるが空き領域がテーブル内に留まっている状態です。標準のVACUUMは末尾の連続した空きしかOSへ返さないため、途中のページに散らばった空きはファイルサイズに残ります。以後の挿入で再利用されるので、更新が継続するテーブルなら放置して差し支えありません。使用パターンが変わって再利用が進まない場合だけ、詰め直しを検討します。

共有ロックを取るVACUUMでもアプリは止まりませんか?

標準のVACUUMが取るのは共有更新排他ロックで、SELECT・INSERT・UPDATE・DELETEは並行して動きます。競合するのはALTER TABLEやCREATE INDEXなど定義を変える操作と、同じテーブルへの別のVACUUMです。移行作業の直前に長時間のVACUUMが走っていると定義変更が待たされるので、作業窓では実行状況を確認しておくと安全でしょう。

パーティションテーブルの親にVACUUMを流せば足りますか?

親を指定すれば配下の子テーブルへ再帰的に処理が及びます。ただし自動実行の判定は子テーブルごとに独立して行われ、親の行数では起動しません。書き込みが集中する直近の区画だけ比率を下げるなど、個別設定は子の側へ当てる必要があります。統計の取得についても、親の統計は自動では更新されない点に注意してください。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2024.11.08 テックブログ OpenAPI GeneratorでJavaコードを自動生成する方法|CLI導入からSpring・ライブラリ選択まで
  3. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  4. 2026.10.08 テックブログ 大阪公立大学のランサムウェア被害と仮想化基盤の停止|全授業休講に至った経緯とバックアップを守る設定
  5. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動

RELATED POSTS 関連記事

目次