- データベース
PostgreSQLのVACUUMとautovacuum|監視SQLと設定値の目安
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つです。
- UPDATE 1回でテーブルは2倍(120 MB → 240 MB)になりました。 UPDATE は「新しい版の追加」だからです。
- VACUUM は dead tuple を0にしますが、ファイルサイズは 481 MB のままです。 空いた領域は「再利用可能」になっただけで、OSには返りません。
- 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期連続の黒字決算。