PostgreSQLを本番環境で運用し始めると、こうした問題に直面します。原因のほとんどは、PostgreSQLのMVCC(マルチバージョン並行性制御)の仕組みとVACUUMの役割を体系的に理解していないことです。
この記事では、VACUUMとautovacuumの動作原理を基礎から解説し、テーブル肥大化とXID周回(Wraparound)障害を防ぐ実践的な設定方法を紹介します。Rocky Linux 9 / Ubuntu 24.04上のPostgreSQL 16で動作確認しています。
この記事のポイント
・VACUUMはデッドタプルを再利用可能にし、VACUUM FULLは物理圧縮するが排他ロックが必要
・autovacuumの起動条件はthreshold + scale_factor × 行数で決まる
・XID周回障害を防ぐにはage(relfrozenxid)を定期監視してFreeze VACUUMを計画的に実行
・大量更新テーブルはALTER TABLEでautovacuumパラメータをテーブル単位で上書きする
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
PostgreSQLでVACUUMが必要になる理由(MVCCとデッドタプル)
PostgreSQLは、同時アクセスの整合性を確保するためにMVCC(Multi-Version Concurrency Control)を採用しています。MVCCでは、行をUPDATEまたはDELETEしても古いバージョンの行は即座には消えません。古いバージョンの行データは「デッドタプル(dead tuple)」として物理的にテーブルファイル内に残り続けます。デッドタプルが蓄積すると、次の3つの問題が起きます。
・テーブル肥大化(Table Bloat):行を削除しても実際のファイルサイズは縮まらず、ディスク容量を圧迫します。
・クエリ速度の低下:シーケンシャルスキャン時にデッドタプルも読み取るため、I/Oが増加します。
・インデックス肥大化:インデックスにも古いバージョンへの参照が残り、同様に肥大化します。
VACUUMはこのデッドタプルを回収し、領域を再利用可能にするための仕組みです。デッドタプルの発生量を整理すると、次のとおりです。
・UPDATE 1行 = 旧バージョンのデッドタプルが1行増える
・DELETE 1行 = 対象行がデッドタプルになる
・autovacuumが回収するまでデッドタプルはディスク領域を占有し続ける
もうひとつ見落とされがちな問題が「トランザクションID周回(Transaction ID Wraparound)」です。PostgreSQLのトランザクションIDは32ビット整数で、約20億トランザクションで一周します。autovacuumはデータの凍結処理(FREEZE)も担当しており、これが滞ると最悪の場合PostgreSQLがデータ保護のために読み取り専用モードへ移行します。詳細は後述のXID周回セクションで解説します。
VACUUMコマンドの種類と実行方法
1. VACUUM — デッドタプルを回収する基本コマンド
通常のVACUUMはデッドタプルを回収し、空き領域として再利用可能にします。テーブルに対してShareUpdateExclusiveLockを取得するため、SELECT・INSERT・UPDATE・DELETEと同時に実行可能です。本番環境でもオンラインで実行できます。-- データベース全体をVACUUM $ sudo -u postgres psql -c "VACUUM VERBOSE;" -- 特定テーブルのみVACUUM $ sudo -u postgres psql -d mydb -c "VACUUM VERBOSE orders;"
INFO: vacuuming "public.orders" INFO: scanned index "orders_pkey" to remove 154230 row versions DETAIL: CPU: user: 0.38 s, system: 0.07 s, elapsed: 1.89 s INFO: "orders": removed 154230 row versions in 1543 pages DETAIL: CPU: user: 0.25 s, system: 0.12 s, elapsed: 2.71 s INFO: "orders": found 154230 removable, 9821034 nonremovable row versions in 28341 pages DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 4021938294 CPU: user: 1.98 s, system: 0.41 s, elapsed: 11.42 s VACUUM
2. VACUUM FULL — テーブルファイルを物理圧縮する
VACUUM FULLはテーブルを書き直し、ファイルを実際に縮小します。ただし、AccessExclusiveLockを取得するためテーブルへのすべてのアクセスがブロックされます。本番環境では原則としてメンテナンス時間帯にのみ実行してください。-- テーブル肥大化が深刻な場合(メンテナンス時間帯に実行) $ sudo -u postgres psql -d mydb -c "VACUUM FULL VERBOSE orders;" INFO: vacuuming "public.orders" INFO: "orders": found 0 removable, 2841023 nonremovable row versions in 38412 pages DETAIL: 0 dead row versions cannot be removed yet. CPU: user: 3.28 s, system: 1.42 s, elapsed: 12.74 s. VACUUM
3. VACUUM ANALYZE — 統計情報もあわせて更新する
VACUUM ANALYZEはVACUUMに加え、クエリプランナーが使用する統計情報も更新します。クエリが突然遅くなった際は、まずこのコマンドを試すのが鉄則です。-- VACUUM + 統計情報更新を同時に実行 $ sudo -u postgres psql -d mydb -c "VACUUM ANALYZE orders;"
autovacuumの動作原理と設定パラメータ
autovacuumはPostgreSQL組み込みのバックグラウンドデーモンで、テーブルのデッドタプルが一定量を超えると自動的にVACUUMを実行します。postgresql.confで挙動を制御できます。1. autovacuumが起動する条件(threshold と scale_factor)
autovacuumがVACUUMを開始するのは、テーブルのデッドタプル数が次の条件を超えたときです。デッドタプル数 > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × テーブル行数デフォルト値(PostgreSQL 16):
・autovacuum_vacuum_threshold:50(最低デッドタプル数)
・autovacuum_vacuum_scale_factor:0.2(テーブル行数の20%)
つまり100万行のテーブルでは、20万行以上のデッドタプルが溜まって初めてautovacuumが起動します。大規模テーブルでは起動が遅れ肥大化が進むため、後述のテーブル単位設定で調整が必要です。
ANALYZE(統計情報更新)のトリガーは別のパラメータで制御されます。
・autovacuum_analyze_threshold:50
・autovacuum_analyze_scale_factor:0.1(テーブル行数の10%)
2. 処理速度のコントロール(cost_delay と cost_limit)
autovacuumはI/Oに負荷をかけないよう、コストベースのthrottlingを使っています。・autovacuum_vacuum_cost_delay:2ms(コスト上限に達したときの休止時間)
・autovacuum_vacuum_cost_limit:200(1サイクルで消費できるコストの上限)
VACUUMがページを読む際のコスト単価:
・バッファキャッシュにあるページ(hit):1
・ディスク読み込みが必要なページ(miss):2
・ダーティページの書き込み(dirty):20
コスト合計が200に達すると2ms休止するため、I/O負荷の高い環境ではautovacuumが遅くなります。追いつかない場合は
autovacuum_vacuum_cost_limit = 400のように上限を緩和するのが安全です。autovacuum_vacuum_cost_delay = 0でthrottlingを無効化することもできますが、autovacuumがディスクI/Oを独占してサービスに影響を与える恐れがあるため、本番環境ではcost_limitの引き上げを優先してください。3. 並列数と起動間隔の調整
・autovacuum_max_workers:3(同時に起動できるautovacuumプロセス数)・autovacuum_naptime:1min(各テーブルをチェックする間隔)
テーブル数が多いデータベースではnaptime内に全テーブルをチェックしきれないことがあります。
autovacuum_naptime = 30sに短縮するか、autovacuum_max_workersを増やして対応します。4. postgresql.confの設定方法と反映手順
まず設定ファイルの場所をpsqlで確認します。-- 設定ファイルの場所を確認する $ sudo -u postgres psql -c "SHOW config_file;" config_file -------------------------------------------- /var/lib/pgsql/16/data/postgresql.conf (1 row)
/etc/postgresql/16/main/postgresql.confになります。設定例(肥大化対策の推奨値):# /var/lib/pgsql/16/data/postgresql.conf # autovacuumを有効化(デフォルト on) autovacuum = on # 同時に動かすautovacuumワーカー数(デフォルト: 3) autovacuum_max_workers = 5 # 次のテーブルを確認するまでの間隔(デフォルト: 1min) autovacuum_naptime = 30s # VACUUM起動閾値(scale_factorのデフォルトは0.2 → 5%に絞る) autovacuum_vacuum_threshold = 50 autovacuum_vacuum_scale_factor = 0.05 # ANALYZE起動閾値(プランナ統計の更新) autovacuum_analyze_threshold = 50 autovacuum_analyze_scale_factor = 0.05 # I/Oコスト制御(cost_limitを上げてautovacuumを速くする) autovacuum_vacuum_cost_delay = 2ms autovacuum_vacuum_cost_limit = 400
$ sudo -u postgres psql -c "SELECT pg_reload_conf();" pg_reload_conf ---------------- t (1 row)
$ sudo -u postgres psql -c " SELECT name, setting, unit FROM pg_settings WHERE name IN ( 'autovacuum_vacuum_scale_factor', 'autovacuum_vacuum_cost_delay', 'autovacuum_vacuum_cost_limit', 'autovacuum_max_workers' );" name | setting | unit ---------------------------------+---------+------ autovacuum_max_workers | 5 | autovacuum_vacuum_cost_delay | 2 | ms autovacuum_vacuum_cost_limit | 400 | autovacuum_vacuum_scale_factor | 0.05 | (4 rows)
XID周回(Wraparound)を防ぐFreeze設定
1. XID周回とは何か
PostgreSQLはすべてのトランザクションに32ビットのID(XID)を付与します。XIDは約21億(2^31)で上限に達し、そのまま放置すると古いタプルが「未来のトランザクション」に見える状態になります。これをXID周回(Wraparound)といいます。PostgreSQLはWraparound寸前になるとデータベースを読み取り専用にして障害を防ぎますが、これは事実上のサービス停止を意味します。VACUUMはタプルに「FROZEN」マークを付け、XIDを無効化することでこの問題を回避します。
2. relfrozenxidの年齢を監視するSQL
次のSQLで、各テーブルがXID周回まであと何トランザクションあるかを確認できます。実運用では定期的に実行して監視する習慣を付けてください。-- XID年齢が古いテーブルを上位10件表示 $ sudo -u postgres psql -d mydb -c " SELECT schemaname, relname, age(relfrozenxid) AS xid_age, pg_size_pretty(pg_total_relation_size(oid)) AS size FROM pg_class WHERE relkind = 'r' ORDER BY age(relfrozenxid) DESC LIMIT 10;" schemaname | relname | xid_age | size ------------+---------------+---------+--------- public | orders | 1842301 | 4821 MB public | order_items | 1839450 | 2341 MB public | users | 983201 | 234 MB public | product_cache | 412050 | 18 MB public | sessions | 387901 | 127 MB (5 rows)
xid_ageがautovacuum_freeze_max_age(デフォルト2億)に近づいているテーブルは優先してVACUUM FREEZEを実行します。3. autovacuum_freeze_max_ageの設計目安
関連するパラメータ(postgresql.conf):・vacuum_freeze_min_age:50000000(5000万)— この年齢未満のタプルはFreeze対象外
・vacuum_freeze_max_age:200000000(2億)— この年齢を超えたタプルをForce Freeze
・autovacuum_freeze_max_age:200000000(2億)— この年齢を超えたテーブルは通常の閾値と無関係にautovacuumを強制起動
トランザクション量の多い環境では
autovacuum_freeze_max_ageを1億5000万程度に下げ、余裕を持ってFreeze VACUUMが走るように設計します。テーブル単位でautovacuumをチューニングする
更新頻度が非常に高い大規模テーブルでは、全体設定だけでは追いつかない場合があります。ALTER TABLEのストレージパラメータでテーブルごとに設定を上書きできます。postgresql.confの変更と異なり、pg_reload_conf()不要で即座に有効になります。-- ordersテーブル:scale_factorを0.02(2%)に下げてautovacuumをより頻繁に起動 ALTER TABLE orders SET ( autovacuum_vacuum_scale_factor = 0.02, autovacuum_analyze_scale_factor = 0.01, autovacuum_vacuum_cost_delay = 2 ); -- 設定を確認 SELECT relname, reloptions FROM pg_class WHERE relname = 'orders'; relname | reloptions -------------+---------------------------------------------------------------------------------- orders | {autovacuum_vacuum_scale_factor=0.02,autovacuum_analyze_scale_factor=0.01,autovacuum_vacuum_cost_delay=2} (1 row)
-- テーブル単位の設定をデフォルトに戻す ALTER TABLE orders RESET (autovacuum_vacuum_scale_factor);
トラブルシュート — autovacuumが追いつかないケース
【確認1】pg_stat_user_tablesでデッドタプルを確認する
-- デッドタプルが多いテーブルを確認 $ sudo -u postgres psql -d mydb -c " SELECT relname, n_live_tup, n_dead_tup, round(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_ratio_pct, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;" relname | n_live_tup | n_dead_tup | dead_ratio_pct | last_autovacuum | last_autoanalyze -------------+------------+------------+----------------+-------------------------------+------------------------------- orders | 9821034 | 1243198 | 11.2 | 2026-08-25 03:14:22.341256+09 | 2026-08-25 03:14:24.881943+09 order_items | 28341029 | 843022 | 2.9 | 2026-08-25 06:42:10.123400+09 | 2026-08-25 06:42:14.334521+09
dead_ratio_pctが10%を超えているテーブルは、autovacuumの起動頻度を上げるかmanual VACUUMを検討します。テーブルの物理サイズと有効行数を突き合わせてbloatの深刻度を確認することもできます。-- テーブルサイズとlive行数の比率でbloatを把握する $ sudo -u postgres psql -d mydb -c " SELECT relname, pg_size_pretty(pg_relation_size(relid)) AS table_size, n_live_tup, pg_size_pretty( pg_relation_size(relid) / NULLIF(n_live_tup, 0) ) AS bytes_per_live_row FROM pg_stat_user_tables WHERE n_live_tup > 0 ORDER BY pg_relation_size(relid) DESC LIMIT 10;"
bytes_per_live_rowが実際のスキーマサイズを大きく上回っている場合、デッドタプルが物理ファイルを圧迫している可能性があります。【確認2】autovacuumが現在実行中かを確認する
-- 実行中のautovacuumプロセスを確認 $ sudo -u postgres psql -c " SELECT pid, datname, query_start, state, query FROM pg_stat_activity WHERE query LIKE 'autovacuum%';" pid | datname | query_start | state | query -------+-----------+-------------------------------+-------+------------------------------------------------------------ 10234 | mydb | 2026-09-24 02:15:01.234+09 | active| autovacuum: VACUUM public.access_logs (to prevent wraparound) 10501 | mydb | 2026-09-24 02:16:23.891+09 | active| autovacuum: VACUUM public.order_items (2 rows)
autovacuum_max_workers)を埋めている場合は、上限を増やすか問題テーブルの分割・アーカイブを検討します。【確認3】手動VACUUMで緊急対応する
autovacuumが追いつかない場合、低トラフィック時間帯に手動VACUUMを実行します。-- メンテナンス時間帯に手動で実行(VERBOSEで進捗確認) $ sudo -u postgres psql -d mydb -c "VACUUM (VERBOSE, ANALYZE) orders;" -- XID年齢が深刻な場合はFREEZEも付ける $ sudo -u postgres psql -d mydb -c "VACUUM (VERBOSE, FREEZE) orders;"
【確認4】長時間トランザクションがVACUUMをブロックしていないか確認する
VACUUMは「全トランザクションが参照しなくなった」デッドタプルしか回収できません。長時間コミットしていないトランザクションがあると、それより前のデッドタプルはすべて回収不能になります。-- 5分以上続いているトランザクションを確認する $ sudo -u postgres psql -c " SELECT pid, now() - xact_start AS duration, state, LEFT(query, 60) AS query_head FROM pg_stat_activity WHERE xact_start IS NOT NULL AND now() - xact_start > interval '5 minutes' ORDER BY duration DESC;" pid | duration | state | query_head ------+------------+-------+------------------------------ 8821 | 00:47:32 | idle | BEGIN 9104 | 00:12:15 | active| SELECT count(*) FROM large_t (2 rows)
SELECT pg_terminate_backend(8821)で強制終了できます。【確認5】autovacuumのログで処理時間を確認する
log_autovacuum_min_durationを設定すると、指定時間以上かかったautovacuumをPostgreSQLのログに記録できます。どのテーブルのVACUUMが時間を要しているかを把握するのに役立ちます。# postgresql.confに追加(250ms以上かかったautovacuumをログに記録) log_autovacuum_min_duration = 250ms
SELECT pg_reload_conf())、ログを確認します。# Rocky Linux 9 / RHEL 9の場合 grep -i autovacuum /var/lib/pgsql/16/data/log/postgresql-2026-09-24_000000.log | tail -10 # Ubuntu 24.04の場合 grep -i autovacuum /var/log/postgresql/postgresql-16-main.log | tail -10
2026-09-24 02:15:33.108 JST [10234] LOG: automatic vacuum of table "mydb.public.order_items": index scans: 1 pages: 0 removed, 18341 remain, 0 skipped due to pins tuples: 182430 removed, 1203551 remain, 0 are dead but not yet removable, oldest xmin: 4021938294 I/O timings: read: 1247.839 ms, write: 92.144 ms avg read rate: 9.813 MB/s, avg write rate: 0.726 MB/s system usage: CPU: user: 1.52 s, sys: 0.53 s, elapsed: 10.22 s
oldest xminの値が長時間進まない場合、長時間トランザクション(【確認4】参照)がデッドタプルの解放を阻んでいます。まとめ
PostgreSQLのVACUUMとautovacuumを適切に設計することは、安定した本番運用の基本です。| やりたいこと | コマンドまたはパラメータ |
|---|---|
| デッドタプルをオンラインで回収する | VACUUM テーブル名 |
| テーブルファイルを物理圧縮する(ロックあり) | VACUUM FULL テーブル名 |
| VACUUM+プランナ統計を同時更新する | VACUUM ANALYZE テーブル名 |
| XID年齢を確認する | SELECT age(relfrozenxid) FROM pg_class WHERE relname='テーブル名' |
| autovacuumの起動頻度を上げる(大規模テーブル) | ALTER TABLE t SET (autovacuum_vacuum_scale_factor=0.02) |
| autovacuumのI/O throttlingを緩和する | autovacuum_vacuum_cost_limit = 400(postgresql.conf) |
| 設定変更を再起動なしで反映する | SELECT pg_reload_conf() |
| テーブル単位の設定をリセットする | ALTER TABLE t RESET (autovacuum_vacuum_scale_factor) |
| デッドタプル比率を確認する | SELECT n_dead_tup FROM pg_stat_user_tables WHERE relname='テーブル名' |
| 長時間トランザクションを確認する | SELECT pid, now()-xact_start FROM pg_stat_activity WHERE xact_start IS NOT NULL |
| autovacuumの処理をログに記録する | log_autovacuum_min_duration = 250ms(postgresql.conf) |
| XID周回を防ぐFreeze VACUUMを強制実行する | VACUUM FREEZE テーブル名 |
autovacuumを無効化したり、VACUUMを完全に手動管理するアプローチは、XID周回障害やテーブル肥大化のリスクを高めます。まずautovacuumを有効にした上で、監視SQLと
pg_stat_user_tablesを使って状況を把握し、問題のあるテーブルだけをテーブル単位でチューニングするのが現場での正攻法です。Linuxサーバー上でPostgreSQLをはじめとするミドルウェアを実運用する際の設計・チューニングのノウハウは、体系的に学ぶことで現場での判断力が大きく変わります。
PostgreSQLのチューニングを本番で活かすには、Linuxサーバー運用の「型」が基礎になります
autovacuumの設計やXID周回対策は、OSのプロセス管理・ファイルシステム・ディスクI/Oを体系的に理解していると判断がしやすくなります。独学で断片的に覚えるより、現場で実際に使われる設計パターンを一度体系的に身につけることで、PostgreSQLも含めたミドルウェア全体の安定運用ができるようになります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。
「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:PostgreSQLのストリーミングレプリケーションを構築する手順|pg_basebackupからスタンバイ昇格まで
- 前のページへ:PostgreSQLをpg_dumpとpg_restoreでバックアップする手順|論理バックアップの取得と復元の実務
- この記事の属するカテゴリ:データーベース管理へ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます