PostgreSQLのVACUUMとautovacuum運用設計|テーブル肥大化とXID周回を防ぐ設定の考え方

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > Linuxtips > データーベース管理 > PostgreSQLのVACUUMとautovacuum運用設計|テーブル肥大化とXID周回を防ぐ設定の考え方
「autovacuumを有効にしているのに、テーブルがどんどん肥大化していく」「VACUUM FULLをかけたら本番テーブルがロックされてサービスが止まった」「XID周回という言葉を聞いたが、自分のサーバーは大丈夫なのか不安だ」

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パラメータをテーブル単位で上書きする


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

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;"

VERBOSEオプションを付けると処理の詳細が出力されます。実機出力例(Rocky Linux 9.4 / PostgreSQL 16.3):

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

「removed XXXX row versions」がデッドタプルの回収数です。注意点として、通常のVACUUMではテーブルファイルの物理サイズは縮小しません。あくまでもデッドタプルのあった領域を「次のINSERT/UPDATEで再利用できる空き領域」としてマーキングするだけです。

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

サービス停止なしでテーブルファイルを縮小したい場合は、オンライン再編成ツールのpg_repackを検討してください。pg_repackは排他ロックを長時間保持せずにVACUUM FULLと同等のファイル縮小を実現します。

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)

Ubuntu 24.04の場合は/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)

反映されたことをpg_settingsで確認します。

$ 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)

1000万行以上のテーブルにはscale_factorを0.01~0.05に下げることを推奨します。デフォルトの0.2では20万行以上のデッドタプルが溜まってから初めてVACUUMが起動するため、肥大化が目立ちやすくなります。設定を元に戻す場合はRESETで削除できます。

-- テーブル単位の設定をデフォルトに戻す 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)

「to prevent wraparound」が付いているものは、XID周回を防ぐための緊急VACUUMです。通常のbloat対策VACUUMより優先度が高く、cost_delayのthrottlingも無視して実行されます。ワーカーが常に全スロット(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)

pid 8821は47分間コミットもROLLBACKもしていないゾンビトランザクションです。アプリ側で接続を切り忘れた典型例です。アプリへの影響を確認した上で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日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。