PostgreSQLのVACUUMとは、UPDATE や DELETE で不要になった古い行(dead tuple)を片付けて再利用できるようにし、あわせてトランザクションIDの周回に備える「凍結(FREEZE)」も行うメンテナンス処理です。 通常は autovacuum(自動VACUUM)がバックグラウンドで実行しますが、更新の多いテーブルや長時間トランザクションがあると片付けが追いつかず、テーブルが大きくなったり検索が遅くなったりします。

この記事では、VACUUM / VACUUM FULL / ANALYZE / autovacuum / FREEZE の違いを表で整理したうえで、PostgreSQL 16.13 で100万行のテーブルに UPDATE・DELETE を繰り返し、dead tuple とサイズがどう変わるかを実測します。さらに autovacuum が動き出す条件の計算と、テーブル単位の設定で早める実験、長時間トランザクションが VACUUM を止める様子の2セッション再現、すぐ使える監視SQL集、規模別の設定値の目安、VACUUM FULL と pg_repack の判断フローまでまとめます。PG17・PG18 の変更点は公式リリースノートで確認した範囲で記載しています。

VACUUM関連コマンドの早見表

種類 何をするか テーブルのサイズ ロック(読み書きは?) 使う場面
VACUUM dead tuple を片付け、空き領域を再利用可能にする。可視性マップ・空き領域マップを更新 基本的に縮まない(末尾の空きページのみ切り詰め) SHARE UPDATE EXCLUSIVE(読み書きOK) 日常の保守。通常は autovacuum に任せる
VACUUM FULL テーブルを丸ごと書き直して詰める 縮む(OSに返す) ACCESS EXCLUSIVE(読み書き不可) 大量削除後に容量を取り戻したいとき
ANALYZE 統計情報を更新(実行計画の材料) 変わらない SHARE UPDATE EXCLUSIVE 大量投入・更新の直後
VACUUM ANALYZE VACUUM と ANALYZE を続けて実行 縮まない SHARE UPDATE EXCLUSIVE 手動で両方まとめて行うとき
VACUUM (FREEZE) 全ての行を積極的に凍結し、XID の年齢をリセット 縮まない SHARE UPDATE EXCLUSIVE 大量ロード後、周回対策を前倒ししたいとき
autovacuum しきい値を超えたテーブルに VACUUM / ANALYZE を自動実行 縮まない SHARE UPDATE EXCLUSIVE 常時(既定で on)
pg_repack(拡張) オンラインでテーブルを作り直して詰める 縮む 開始・終了時に短時間だけ強いロック 止められないテーブルの容量回収

迷ったら「日常は autovacuum、容量を取り戻したいときだけ VACUUM FULL か pg_repack」と覚えておけば十分です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQLのVACUUMとautovacuum|監視SQLと設定値の目安」

なぜVACUUMが必要か(MVCCとdead tuple)

PostgreSQL は UPDATE のたびに行を上書きせず、新しい版の行を追加して古い版を「不要」と印を付けます(MVCC:多版型同時実行制御)。 実行中の他のトランザクションが古い版を見ている可能性があるためです。DELETE も同様に、行は印が付くだけで物理的には残ります。

flowchart LR
    A[UPDATE qty=101] --> B[旧版 qty=100\n→ dead tuple]
    A --> C[新版 qty=101\n→ live tuple]
    B --> D{VACUUM}
    D -->|誰も参照していない| E[空き領域として再利用]
    D -->|古いスナップショットが参照中| F[消せずに残る]
用語 意味
live tuple 現在有効な行
dead tuple UPDATE・DELETE で不要になった古い行の版
可視性マップ 「全行が全員から見えるページ」の印。Index Only Scan や VACUUM の省略に使う
XID(トランザクションID) 32ビットの通し番号。約21億で一周する
凍結(FREEZE) 古い行に「十分古い」印を付け、XID が一周しても正しく見えるようにする処理
relfrozenxid / datfrozenxid テーブル/DB で、凍結されていない最古の XID。age() で年齢がわかる

VACUUM の役割は「dead tuple の片付け」「凍結によるXID周回対策」「可視性マップの更新」の3つです。 ANALYZE(統計情報の更新)は別の処理ですが、autovacuum が一緒に担当します。

実測:dead tupleとテーブルサイズの推移

ここからは PostgreSQL 16.13(Ubuntu、2コア・メモリ7GB、既定設定) での実行結果です。変化を観察しやすくするため、このテーブルだけ autovacuum を止めています。

CREATE TABLE stock (
  item_id  int PRIMARY KEY,
  qty      int NOT NULL,
  note     text NOT NULL
) WITH (autovacuum_enabled = false);   -- 実験のため自動VACUUMを止める

INSERT INTO stock SELECT g, 100, repeat('x', 80) FROM generate_series(1, 1000000) g;
ANALYZE stock;

-- 観察用ビュー
CREATE VIEW v_stat AS
SELECT n_live_tup, n_dead_tup,
       pg_size_pretty(pg_table_size('stock'))   AS table_size,
       pg_size_pretty(pg_indexes_size('stock')) AS index_size,
       last_vacuum, last_autovacuum
FROM pg_stat_user_tables WHERE relname = 'stock';

SELECT pg_stat_force_next_flush();   -- 統計を即時反映(PG15〜)
SELECT * FROM v_stat;

その後、全件 UPDATE を3回、半分の行の DELETE、VACUUM、再度の UPDATE、VACUUM FULL の順に実行し、各段階で v_stat を確認しました。

UPDATE stock SET qty = qty + 1;                 -- ×3回
DELETE FROM stock WHERE item_id % 2 = 0;        -- 50万行削除
VACUUM (VERBOSE) stock;
UPDATE stock SET qty = qty + 1;                 -- 残り50万行を更新
VACUUM stock;
VACUUM (FULL, VERBOSE) stock;
段階 n_live_tup n_dead_tup テーブル インデックス
① 100万行投入直後 1,000,000 0 120 MB 21 MB
② 全件UPDATE 1回 1,000,000 1,000,000 240 MB 43 MB
③ 全件UPDATE 計3回 1,000,000 3,000,000 481 MB 54 MB
④ 半分DELETE 500,000 3,499,946 481 MB 54 MB
⑤ VACUUM 後 500,000 0 481 MB(縮まない) 54 MB
⑥ その後50万行UPDATE 500,000 500,000 481 MB(増えない) 54 MB
⑦ VACUUM FULL 後 ― 0 60 MB 11 MB

ここから読み取れることは3つです。

  1. UPDATE 1回でテーブルは2倍(120 MB → 240 MB)になりました。 UPDATE は「新しい版の追加」だからです。
  2. VACUUM は dead tuple を0にしますが、ファイルサイズは 481 MB のままです。 空いた領域は「再利用可能」になっただけで、OSには返りません。
  3. VACUUM 後の UPDATE は空き領域を再利用するため、サイズが増えませんでした(⑥)。 autovacuum が適切に動いていれば、テーブルは一定のサイズで安定します。

⑦の VACUUM FULL で、ようやく 481 MB → 60 MB(50万行分)まで縮みました。なお VACUUM FULL 直後の n_live_tup は 874,013 という推定値になっていたため表では省略しています。ANALYZE stock; を実行すると 500,000 に更新されました。n_live_tup・n_dead_tup は統計上の推定値なので、正確な件数が必要なときは count(*) で確認します。

VACUUM (VERBOSE) の出力の読み方

⑤の VACUUM(約5.8秒)の出力です。

INFO:  vacuuming "t88.public.stock"
INFO:  finished vacuuming "t88.public.stock": index scans: 1
pages: 0 removed, 61538 remain, 61538 scanned (100.00% of total)
tuples: 500000 removed, 500000 remain, 0 are dead but not yet removable
removable cutoff: 35855, which was 0 XIDs old when operation ended
new relfrozenxid: 35853, which is 11 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan needed: 61538 pages from table (100.00% of total) had 3499946 dead item identifiers removed
index "stock_pkey": pages: 6926 in total, 0 newly deleted, 0 currently deleted, 0 reusable
avg read rate: 131.404 MB/s, avg write rate: 73.497 MB/s
system usage: CPU: user: 0.64 s, system: 0.29 s, elapsed: 5.79 s
行 意味 注目点
pages: 0 removed 末尾の空きページを切り詰めた数 0 ならファイルは縮んでいない
tuples: ... removed 片付けた行数 ―
dead but not yet removable 消したいが消せなかった行数 0 より大きければ長時間トランザクション等を疑う
removable cutoff これより古い XID の dead tuple は消せる 現在の XID と差が大きいと要注意
index scans: 1 インデックス掃除の周回数 2以上ならメモリ(maintenance_work_mem)不足の可能性
dead item identifiers removed 行ポインタの解放数 ―

VACUUM FULL の出力

INFO:  vacuuming "public.stock"
INFO:  "public.stock": found 0 removable, 500000 nonremovable row versions in 61538 pages
DETAIL:  0 dead row versions cannot be removed yet.
VACUUM
Time: 1228.145 ms (00:01.228)

VACUUM FULL はテーブルとインデックスを新しいファイルに書き直すため、実行中はそのテーブルへの SELECT も待たされます。 また、書き直し先の容量(最大でテーブル+インデックス分)が一時的に必要です。

autovacuumの発火条件を計算する

計算式

autovacuum は autovacuum_naptime(既定1分)ごとに各テーブルを確認し、次の条件を満たしたテーブルを処理します。

処理 発火条件(公式ドキュメントの式) 既定値
VACUUM(更新・削除) dead tuple 数 > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × 行数(reltuples) 50 + 0.2 × 行数
VACUUM(挿入、PG13〜) 前回VACUUM以降の挿入数 > autovacuum_vacuum_insert_threshold + autovacuum_vacuum_insert_scale_factor × 行数 1000 + 0.2 × 行数(PG18 では凍結されていないページの割合を使う)
ANALYZE 前回ANALYZE以降の変更行数 > autovacuum_analyze_threshold + autovacuum_analyze_scale_factor × 行数 50 + 0.1 × 行数
周回対策 VACUUM age(relfrozenxid) > autovacuum_freeze_max_age 2億(autovacuum が off でも実行される)
行数 VACUUM が動く dead tuple 数(既定) 割合
1万行 2,050 約20%
10万行 20,050 約20%
100万行 200,050 約20%
1億行 20,000,050 約20%
10億行 200,000,050(PG18 は上限 1億) 約20%(PG18 は10%)

既定では「約20%が dead tuple になるまで待つ」ため、大きなテーブルほど1回の autovacuum が重くなります。 PG18 では autovacuum_vacuum_max_threshold(既定 100,000,000)が加わり、しきい値に上限が付くようになりました。

実験:既定のテーブルとチューニングしたテーブル

10万行のテーブルを2つ用意し、片方だけテーブル単位でしきい値を下げました。

CREATE TABLE av_default (task_id int PRIMARY KEY, status int NOT NULL);
CREATE TABLE av_tuned   (task_id int PRIMARY KEY, status int NOT NULL);
INSERT INTO av_default SELECT g, 0 FROM generate_series(1,100000) g;
INSERT INTO av_tuned   SELECT g, 0 FROM generate_series(1,100000) g;
-- (数秒待ってから)
VACUUM ANALYZE av_default; VACUUM ANALYZE av_tuned;

-- テーブル単位で発火を早める: 100 + 0.01 × 100000 = 1,100
ALTER TABLE av_tuned SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_threshold = 100);

UPDATE av_default SET status = 1 WHERE task_id <= 15000;   -- 15,000 dead(しきい値 20,050 未満)
UPDATE av_tuned   SET status = 1 WHERE task_id <= 2000;    --  2,000 dead(しきい値 1,100 超え)

発火までの残りを確認するSQLです(テーブル単位の設定があればそれを、なければ全体設定を使います)。

SELECT s.relname, s.n_live_tup, s.n_dead_tup,
       c.reltuples::bigint AS reltuples,
       coalesce((SELECT split_part(o,'=',2)::int FROM unnest(c.reloptions) o
                 WHERE o LIKE 'autovacuum_vacuum_threshold=%'),
                current_setting('autovacuum_vacuum_threshold')::int) AS base,
       coalesce((SELECT split_part(o,'=',2)::float8 FROM unnest(c.reloptions) o
                 WHERE o LIKE 'autovacuum_vacuum_scale_factor=%'),
                current_setting('autovacuum_vacuum_scale_factor')::float8) AS scale,
       s.last_autovacuum, s.autovacuum_count
FROM pg_stat_user_tables s JOIN pg_class c ON c.oid = s.relid
WHERE s.relname IN ('av_default','av_tuned') ORDER BY 1;

UPDATE(14:05:36)から約1分40秒後の結果:

  relname   | n_live_tup | n_dead_tup | reltuples | base | scale |        last_autovacuum        | autovacuum_count
------------+------------+------------+-----------+------+-------+-------------------------------+------------------
 av_default |     100000 |      15000 |    100000 |   50 |   0.2 |                               |                0
 av_tuned   |      99885 |          0 |     99885 |  100 |  0.01 | 2026-10-01 14:05:48.129052+00 |                1

チューニングした av_tuned は UPDATE の約12秒後に autovacuum が動きました。既定の av_default は 15,000 dead のまま待機しています。 続けて av_default を 6,000 行追加で更新し、合計 21,000(しきい値 20,050 超え)にしました。

UPDATE av_default SET status = 2 WHERE task_id BETWEEN 15001 AND 21000;   -- 14:07:24
  relname   | n_live_tup | n_dead_tup | reltuples | base | scale |        last_autovacuum        | autovacuum_count
------------+------------+------------+-----------+------+-------+-------------------------------+------------------
 av_default |      89692 |          0 |     89692 |   50 |   0.2 | 2026-10-01 14:07:48.19541+00  |                1

しきい値を超えた約24秒後に autovacuum が実行されました。 計算式どおりの動きです(autovacuum 後の reltuples は VACUUM が見積もり直した推定値です)。

実験の注意:INSERT 直後すぐに手動 VACUUM すると、INSERT 側の統計がまだ反映されておらず(統計は最短1秒間隔でまとめて送られます)、「挿入数のしきい値」で autovacuum が先に動くことがありました。手順を確かめるときは、各段階の間に数秒あけると結果が安定します。

テーブル単位の設定(ALTER TABLE ... SET)

-- 更新の多いテーブル:1%+1000行で発火、待ち時間なし
ALTER TABLE app_orders SET (
  autovacuum_vacuum_scale_factor  = 0.01,
  autovacuum_vacuum_threshold     = 1000,
  autovacuum_analyze_scale_factor = 0.02,
  autovacuum_vacuum_cost_delay    = 0
);

-- 設定の確認
SELECT relname, reloptions FROM pg_class WHERE reloptions IS NOT NULL;

-- 元に戻す
ALTER TABLE app_orders RESET (autovacuum_vacuum_scale_factor, autovacuum_vacuum_threshold);

全体設定(postgresql.conf)を変えると全テーブルに影響しますが、テーブル単位の設定なら更新の多いテーブルだけを手厚くできます。 行数が多く更新も多いテーブルから順に設定するのが効果的です。

長時間トランザクションがVACUUMを妨げる例(2セッションで再現)

VACUUM は「どこかで実行中のトランザクションがまだ見ている可能性がある行」を消せません。 開始したまま放置されたトランザクション(idle in transaction)があると、その間に生まれた dead tuple は全テーブルで残り続けます。

セッションA(トランザクションを開始して放置):

BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM stock;     -- スナップショットを取得
-- ここで COMMIT せずに放置

セッションB(削除して VACUUM):

DELETE FROM stock WHERE item_id <= 100000;    -- 50,000行
VACUUM (VERBOSE) stock;
DELETE 50000
INFO:  finished vacuuming "t88.public.stock": index scans: 0
pages: 0 removed, 7693 remain, 7693 scanned (100.00% of total)
tuples: 0 removed, 500000 remain, 50000 are dead but not yet removable
removable cutoff: 35924, which was 1 XIDs old when operation ended

50000 are dead but not yet removable ――削除した5万行が1行も片付いていません。 原因のセッションは pg_stat_activity で見つけられます。

SELECT pid, state, backend_xmin, age(backend_xmin) AS xmin_age,
       now() - xact_start AS xact_duration, left(query, 40) AS query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL AND pid <> pg_backend_pid()
ORDER BY age(backend_xmin) DESC;
  pid  |        state        | backend_xmin | xmin_age |  xact_duration  |            query
-------+---------------------+--------------+----------+-----------------+-----------------------------
 26196 | idle in transaction |        35924 |        1 | 00:00:02.089749 | SELECT count(*) FROM stock;

セッションAが COMMIT した後、もう一度 VACUUM すると片付きました。

tuples: 50000 removed, 404971 remain, 0 are dead but not yet removable
VACUUM を妨げるもの 確認する場所 対処
長時間・放置トランザクション pg_stat_activity の backend_xmin・state = 'idle in transaction' アプリで確実に COMMIT/idle_in_transaction_session_timeout を設定
長時間の SELECT(REPEATABLE READ・バッチ) 同上(xact_start が古い) バッチを分割/statement_timeout
準備済みトランザクション(2PC) pg_prepared_xacts COMMIT PREPARED / ROLLBACK PREPARED
レプリケーションスロット pg_replication_slots の xmin・catalog_xmin 使っていないスロットを削除
スタンバイの hot_standby_feedback = on pg_stat_replication の backend_xmin スタンバイ側の長時間クエリを確認
-- 例:開始したまま5分以上放置されたトランザクションを自動で終了(DB単位で設定)
ALTER DATABASE appdb SET idle_in_transaction_session_timeout = '5min';

監視SQL集

dead tuple が多いテーブル上位

SELECT schemaname || '.' || relname AS table_name,
       n_live_tup, n_dead_tup,
       round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
       pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
       last_vacuum, last_autovacuum, autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 5;
    table_name     | n_live_tup | n_dead_tup | dead_pct | total_size |          last_vacuum          |        last_autovacuum        | autovacuum_count
-------------------+------------+------------+----------+------------+-------------------------------+-------------------------------+------------------
 public.stock      |     404971 |     899972 |     69.0 | 183 MB     | 2026-10-01 14:09:23.567097+00 |                               |                0
 public.av_default |      89692 |          0 |      0.0 | 6992 kB    | 2026-10-01 14:05:33.68515+00  | 2026-10-01 14:07:48.19541+00  |                1
 public.av_tuned   |      99885 |          0 |      0.0 | 5904 kB    | 2026-10-01 14:05:33.731047+00 | 2026-10-01 14:05:48.129052+00 |                1

dead_pct が高いのに last_autovacuum が空・古いテーブルは、autovacuum が止められているか追いついていません。 この例の stock は autovacuum を止めているため、dead tuple が69%まで溜まっています。

XID 年齢(周回対策の監視)

SELECT datname, age(datfrozenxid) AS xid_age,
       round(100.0 * age(datfrozenxid) / current_setting('autovacuum_freeze_max_age')::int, 2) AS pct_of_freeze_max_age,
       round(100.0 * age(datfrozenxid) / 2147483648, 4) AS pct_of_wraparound
FROM pg_database ORDER BY 2 DESC;
  datname  | xid_age | pct_of_freeze_max_age | pct_of_wraparound
-----------+---------+-----------------------+-------------------
 t88       |   35205 |                  0.02 |            0.0016
 postgres  |   35205 |                  0.02 |            0.0016
 template1 |   35205 |                  0.02 |            0.0016
 ...(一部抜粋)
-- テーブル単位(年齢の大きい順)
SELECT c.oid::regclass AS table_name, age(c.relfrozenxid) AS xid_age,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_class c WHERE c.relkind IN ('r','m','t')
ORDER BY age(c.relfrozenxid) DESC LIMIT 10;
age(datfrozenxid) 状態 目安の対応
〜2億(autovacuum_freeze_max_age 未満) 通常 監視のみ
2億超え 周回対策の autovacuum が自動で実行される 進んでいるか pg_stat_progress_vacuum で確認
10億超え 対策が追いついていない 原因(長時間トランザクション等)を取り除き、手動で VACUUM (FREEZE)
約20億に接近 書き込みを守るため新しい XID の発行が止まる 停止前に計画的な VACUUM を行う

PG14 以降は vacuum_failsafe_age(既定16億)を超えると、VACUUM がコスト制限を外して周回対策を最優先する仕組みもあります。監視では「2億を超えたら注意、10億で対応」をアラートの基準にすると運用しやすくなります。

凍結の効果は手動の VACUUM (FREEZE) で確かめられます。

VACUUM (FREEZE) stock;
SELECT c.oid::regclass, age(c.relfrozenxid) FROM pg_class c WHERE relname = 'stock';
  oid  | age
-------+-----
 stock |   0

実行中の VACUUM の進み具合(pg_stat_progress_vacuum)

SELECT p.pid, p.relid::regclass AS table_name, p.phase,
       p.heap_blks_total, p.heap_blks_scanned, p.heap_blks_vacuumed,
       round(100.0 * p.heap_blks_scanned / nullif(p.heap_blks_total, 0), 1) AS scanned_pct,
       p.index_vacuum_count, p.max_dead_tuples, p.num_dead_tuples,
       now() - a.xact_start AS elapsed
FROM pg_stat_progress_vacuum p JOIN pg_stat_activity a USING (pid);

コスト遅延を大きくして(SET vacuum_cost_delay = 20)ゆっくり動く VACUUM を作り、5秒間隔で2回確認しました(psql の \x 表示)。

-[ RECORD 1 ]------+---------------
pid                | 26256
table_name         | stock
phase              | scanning heap
heap_blks_total    | 20769
heap_blks_scanned  | 7903
heap_blks_vacuumed | 0
scanned_pct        | 38.1
index_vacuum_count | 0
max_dead_tuples    | 6043779
num_dead_tuples    | 513597
elapsed            | 00:00:03.00973
-[ RECORD 1 ]------+----------------
...
heap_blks_scanned  | 10123
scanned_pct        | 48.7
num_dead_tuples    | 657897
elapsed            | 00:00:08.069922
phase 意味
initializing 準備中
scanning heap テーブルを読み、dead tuple を集めている
vacuuming indexes インデックスから dead tuple を削除中(index_vacuum_count が増える)
vacuuming heap テーブル本体の行を片付け中
cleaning up indexes / truncating heap 後処理・末尾ページの切り詰め

PG17 では列名が変わり、max_dead_tuples → max_dead_tuple_bytes、num_dead_tuples → num_dead_item_ids となり、indexes_total・indexes_processed が追加されています。 監視ツールのSQLはバージョンに合わせて書き換えてください。

いま動いている autovacuum

SELECT pid, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE backend_type = 'autovacuum worker';

query 列に autovacuum: VACUUM public.テーブル名 のように表示されます(周回対策の場合は (to prevent wraparound) が付きます)。

現在の設定値(PG16 の既定)

SELECT name, setting, unit FROM pg_settings
WHERE name LIKE 'autovacuum%' OR name IN ('maintenance_work_mem','vacuum_cost_limit',
  'log_autovacuum_min_duration','idle_in_transaction_session_timeout')
ORDER BY name;
                 name                  |  setting  | unit
---------------------------------------+-----------+------
 autovacuum                            | on        |
 autovacuum_analyze_scale_factor       | 0.1       |
 autovacuum_analyze_threshold          | 50        |
 autovacuum_freeze_max_age             | 200000000 |
 autovacuum_max_workers                | 3         |
 autovacuum_naptime                    | 60        | s
 autovacuum_vacuum_cost_delay          | 2         | ms
 autovacuum_vacuum_cost_limit          | -1        |
 autovacuum_vacuum_insert_scale_factor | 0.2       |
 autovacuum_vacuum_insert_threshold    | 1000      |
 autovacuum_vacuum_scale_factor        | 0.2       |
 autovacuum_vacuum_threshold           | 50        |
 autovacuum_work_mem                   | -1        | kB
 idle_in_transaction_session_timeout   | 0         | ms
 log_autovacuum_min_duration           | 600000    | ms
 maintenance_work_mem                  | 65536     | kB
 vacuum_cost_limit                     | 200       |

(log_autovacuum_min_duration は本記事の環境の値です。公式の既定は PG15 以降 10分=600000 ms です。)

規模別の設定値の目安

以下は公式ドキュメントの各パラメータの意味をもとにした出発点の目安です。 実際には監視SQLで dead tuple の推移と autovacuum の実行時間を見ながら調整してください。

設定 既定 小規模(〜数GB、更新少) 中規模(数十GB、更新多め) 大規模(数百GB〜、更新多)
autovacuum_max_workers 3 3 3〜5 5〜10(PG18 は再起動なしで変更可)
autovacuum_naptime 1min 1min 1min 15〜30s
autovacuum_vacuum_scale_factor 0.2 0.2 0.05〜0.1 大きなテーブルは個別に 0.01〜0.02
autovacuum_vacuum_threshold 50 50 50〜1000 個別に 1000〜10000
autovacuum_analyze_scale_factor 0.1 0.1 0.05 個別に 0.01〜0.02
autovacuum_vacuum_cost_limit -1(=200) -1 1000〜2000 2000〜(ワーカー数で分け合う)
autovacuum_vacuum_cost_delay 2ms 2ms 2ms 0〜2ms
maintenance_work_mem / autovacuum_work_mem 64MB / -1 64〜256MB 512MB〜1GB 1GB〜(PG17 から1GB の上限が撤廃)
log_autovacuum_min_duration 10min 10min 1min 0〜10s(全件記録して傾向を見る)
idle_in_transaction_session_timeout 0(無効) 10min 5min 1〜5min

大規模ほど「全体の scale_factor を下げる」より「大きく更新の多いテーブルだけを ALTER TABLE ... SET で個別に下げる」方が、他のテーブルに余計な負荷をかけずに済みます。 autovacuum_vacuum_cost_limit は全ワーカーで分け合う値なので、ワーカー数を増やしたらあわせて上げるのがポイントです。

VACUUM FULLとpg_repackの判断フロー

flowchart TD
    A[テーブルが大きくなった] --> B{dead tuple が多い?\nn_dead_tup / dead_pct}
    B -->|多い| C[まず VACUUM\n→ 長時間トランザクションを確認]
    B -->|少ない| D{空き領域が多い?\nサイズに比べて行数が少ない}
    C --> D
    D -->|いいえ| E[通常の増加。対応不要]
    D -->|はい| F{今後また増える?\n同程度のUPDATEが続く}
    F -->|はい| G[VACUUM のまま再利用に任せる\nautovacuum を個別調整]
    F -->|いいえ・容量を戻したい| H{停止時間を取れる?}
    H -->|取れる| I[VACUUM FULL\nACCESS EXCLUSIVE・一時領域が必要]
    H -->|取れない| J[pg_repack\n主キーか一意インデックスが必要]
比較項目 VACUUM FULL pg_repack(拡張)
提供元 PostgreSQL 本体 外部の拡張(別途インストール)
実行中の読み書き できない(ACCESS EXCLUSIVE) できる(開始・終了時に短時間の強いロック)
必要な空き容量 テーブル+インデックス分 テーブル+インデックス分(+変更ログ)
前提条件 なし 主キーまたは NOT NULL の一意インデックス
所要時間の目安 本記事の50万行・481MB で 1.2 秒 VACUUM FULL より長い(変更の追い付き処理があるため)
向いている場面 メンテナンス時間を取れる 止められないテーブル

本記事では pg_repack は未実行です(拡張の導入が必要なため)。使う場合は、PostgreSQL のバージョンに対応した pg_repack をサーバーに導入し、CREATE EXTENSION pg_repack; の後に pg_repack -d appdb -t public.stock のように実行します。どちらを使う場合も、先に「なぜ膨らんだか(長時間トランザクション・autovacuum の遅れ)」を解消しておくと、再び大きくなるのを防げます。

PG13〜PG18 のVACUUM関連の変更

バージョン 主な変更
PG13 挿入件数による autovacuum(autovacuum_vacuum_insert_threshold)、インデックスの並列 VACUUM(VACUUM (PARALLEL n))
PG14 周回対策のフェイルセーフ(vacuum_failsafe_age)、INDEX_CLEANUP に auto(既定)を追加し、不要なインデックス掃除を省略
PG15 統計情報を共有メモリで管理(統計コレクタ廃止)、pg_stat_force_next_flush()
PG16 VACUUM (BUFFER_USAGE_LIMIT)、SKIP_DATABASE_STATS / ONLY_DATABASE_STATS オプション
PG17 dead tuple の記録方式を刷新しメモリ消費を削減(maintenance_work_mem の実質1GB上限を撤廃)、pg_stat_progress_vacuum の列名変更と indexes_total・indexes_processed 追加、vacuum_buffer_usage_limit の既定を 2MB に、MAINTAIN 権限と pg_maintain ロール
PG18 通常の VACUUM でも一部の全可視ページを先行して凍結(vacuum_max_eager_freeze_failure_rate、既定 0.03)、autovacuum_vacuum_max_threshold(既定 1億)、autovacuum_worker_slots で autovacuum_max_workers を再起動なしで変更可能、pg_stat_all_tables に total_vacuum_time・total_autovacuum_time などを追加、track_cost_delay_timing、VACUUM ONLY/ANALYZE ONLY、vacuum_truncate のサーバー設定化

PG18 の autovacuum_vacuum_max_threshold は、「大きなテーブルほど autovacuum が遅れる」という既定の式の性質を、設定1つで和らげるためのものです。 PG18 に上げる予定があれば、テーブル単位の設定とあわせて見直すとよいでしょう。

よくある質問

Q. VACUUM を手動で定期実行する必要はありますか? A. 通常は autovacuum に任せれば十分です。手動が有効なのは、大量ロードや大量削除の直後(VACUUM ANALYZE)、夜間のバッチ後に翌朝の検索を安定させたいとき、周回対策を前倒ししたいとき(VACUUM (FREEZE))です。

Q. VACUUM したのにテーブルが小さくなりません。 A. 通常の VACUUM は空き領域を「再利用可能」にするだけで、ファイルは縮みません(本記事の実測でも 481 MB のまま)。縮める必要があるのは、大量削除後にその容量を二度と使わない場合で、VACUUM FULL か pg_repack を使います。今後も同程度の更新が続くなら、空き領域を再利用させる方が効率的です。

Q. dead but not yet removable が減りません。 A. どこかのトランザクション・レプリケーションスロット・準備済みトランザクションが古いスナップショットを保持しています。本記事の「VACUUM を妨げるもの」の表の順に pg_stat_activity・pg_replication_slots・pg_prepared_xacts を確認してください。

Q. autovacuum をオフにしてもよいですか? A. 一時的な大量ロード中にテーブル単位で autovacuum_enabled = false にすることはありますが、終わったら必ず戻し、手動で VACUUM ANALYZE します。なお、周回対策の VACUUM は autovacuum を off にしていても実行されます。

まとめ

  • VACUUM は dead tuple を片付けて再利用可能にする処理で、ファイルは縮まない。縮めるのは VACUUM FULL(読み書き停止)か pg_repack
  • 実測では UPDATE 1回でテーブルが 120 MB → 240 MB に倍増し、3回で 481 MB。VACUUM 後は再利用で増えず、VACUUM FULL で 60 MB
  • autovacuum は「threshold + scale_factor × 行数」を超えると動く。既定は約20%なので、大きなテーブルは ALTER TABLE ... SET で個別に早める
  • 長時間・放置トランザクションがあると dead but not yet removable になり片付かない。pg_stat_activity の backend_xmin で特定し、idle_in_transaction_session_timeout で防ぐ
  • 監視は「dead tuple 上位」「age(datfrozenxid)」「pg_stat_progress_vacuum」の3本。XID 年齢は2億で注意、10億で対応
  • PG17 で VACUUM のメモリ効率が上がり、PG18 で autovacuum_vacuum_max_threshold や先行凍結が加わった

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQLのVACUUMとautovacuum|監視SQLと設定値の目安」

株式会社RJCでは、PostgreSQLのVACUUMと運用監視を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?