インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

PostgreSQL 18 のVACUUMとANALYZE:肥大化防止と性能維持

公開
現場で迷わない PostgreSQL 18 構築・運用ガイド 第6回 アイキャッチ:VACUUMの仕組みと肥大化防止設計



PostgreSQLの構築を終え、本番サービスを運用し始めて数ヶ月が経過した頃、多くのインフラエンジニアが直面する不可解な現象があります。「データ件数はほとんど増えていないのに、ディスク使用量が右肩上がりに増え続けている」「以前はミリ秒単位で返ってきたSQLの応答が、徐々に遅くなってきた」という事象です。これらの現象の背景には、PostgreSQLの根幹を支える追記型アーキテクチャ(MVCC)と、それに伴って発生する「不要タプル(Dead Tuple)」の蓄積があります。

データベースの自浄作用を司る機能が「VACUUM」と「ANALYZE」です。両者の役割を混同したり、安易に「VACUUM FULL」を実行して本番システムを停止させてしまうトラブルは実務で後を絶ちません。本稿では、不要タプルが発生する内部メカニズムを解読し、通常VACUUMとANALYZEの明確な責務分担、自動バキューム(autovacuum)のチューニング計算式、そして日常の健康状態を監視するスロークエリログの設計手法を整理します。

本記事の到達目標

  • MVCC(多版型同時実行制御)により、UPDATEやDELETEで不要タプル(Dead Tuple)が生じる内部構造を説明できる。
  • 通常VACUUMとANALYZEの役割の違い、および「通常VACUUMでOS空き容量が減らない理由」を説明できる。
  • なぜ本番環境でVACUUM FULLの安易な実行が危険視されるのか(ロックレベルとディスク枯渇リスク)を理解できる。
  • autovacuumデーモンの起動判定計算式を理解し、大規模テーブルでの設定方針を判断できる。
  • log_min_duration_statementを用いたスロークエリログ監視基盤を構築できる。

検証環境情報

  • ホストOS:AlmaLinux 10.2 (Lavender Lion) x86_64
  • DBサーバーノード:alma-db01 (IP: 192.168.2.132 / PostgreSQL 18.6 PGDG)
  • クライアントノード:alma-proxy (IP: 192.168.2.121 / psql クライアント)
目次
  1. 1. PostgreSQLの追記型アーキテクチャと不要タプル(Dead Tuple)
    1. 1.1 MVCC(多版型同時実行制御)の基礎:UPDATEは「DELETE + INSERT」
    2. 1.2 なぜ削除・更新されたデータは即座に消えないのか
    3. 1.3 デッドタプルが引き起こす2大弊害
    4. 1.4 pg_stat_user_tables ビューで不要タプル数を可視化する
    5. 1.5 行ヘッダ(xmin/xmax)と非表示システム列の実機解剖
  2. 2. VACUUM と ANALYZE の本質的相違
    1. 2.1 VACUUM(通常バキューム)の役割
    2. 2.2 なぜ通常VACUUMを実行しても「OSのディスク容量」は減らないのか
    3. 2.3 ANALYZE:オプティマイザの頭脳となる統計情報の最新化
    4. 2.4 定常メンテナンスとしての VACUUM ANALYZE
  3. 3. なぜ本番環境で「VACUUM FULL」は危険とされるのか
    1. 3.1 危険な理由1:ACCESS EXCLUSIVE による読み書き全面停止
    2. 3.2 危険な理由2:テーブル全件コピーによる一時ディスク容量の倍増
  4. 4. autovacuum(自動バキューム)の動作メカニズムとチューニング
    1. 4.1 autovacuum デーモンの起動判定式
    2. 4.2 大規模テーブルで発生する「自動実行の遅延問題」
    3. 4.3 autovacuum の稼働状況を監視するログパラメータ
  5. 5. 【実機検証】デッドタプル発生・回収とスロークエリログの採取
    1. 5.1 大量UPDATEによる不要タプルの生成
    2. 5.2 VACUUM (VERBOSE) による不要タプルの回収
    3. 5.3 スロークエリログ(log_min_duration_statement)の実動確認
    4. 5.4 HOT(Heap-Only Tuple)更新の最適化と fillfactor 設計
  6. 6. 実務トラブルシューティング事例集(3大障害シナリオと解決策)
    1. 6.1 事例1:autovacuum が動作しているのに不要タプルが回収されない
    2. 6.2 事例2:トランザクションID周回障害(TXID Wraparound)と緊急Freeze
    3. 6.3 事例3:テーブル肥大化(Bloat)のオンライン解消(pg_repackの活用)
  7. 7. 実務チートシート:日常保守・監視用SQL集
    1. 7.1 稼働中VACUUMのリアルタイム進捗監視(pg_stat_progress_vacuum)
    2. 7.2 無停止インデックス再構築(REINDEX CONCURRENTLY)の実務手順
  8. 8. 理解度確認演習問題(全3問・解答解説付き)
  9. 9. まとめと次回予告

1. PostgreSQLの追記型アーキテクチャと不要タプル(Dead Tuple)

1.1 MVCC(多版型同時実行制御)の基礎:UPDATEは「DELETE + INSERT」

PostgreSQLでは、複数のトランザクションが同時にデータを読み書きしても競合による遅延が発生しないよう、MVCC(Multi-Version Concurrency Control: 多版型同時実行制御)という仕組みを採用しています。「読み取りは書き込みをブロックせず、書き込みは読み取りをブロックしない」という高い同時実行性能を実現する代償として、PostgreSQLは「追記型アーキテクチャ」を採用しています。

他のRDBMS(MySQL InnoDBなど)のように既存のデータ領域をその場で上書き(In-place update)し、古いデータをアンドゥログへ退避させる方式とは異なり、PostgreSQLではUPDATE処理を「既存行の無効化(DELETE) + 新規行の追記(INSERT)」として処理します。

PostgreSQL MVCCにおけるUPDATEの内部構造とVACUUMによる領域回収(Dead TupleからFSMへの遷移)
図1: PostgreSQL MVCCにおけるUPDATEの内部構造とVACUUMによる領域回収

1.2 なぜ削除・更新されたデータは即座に消えないのか

テーブルの各行(タプル)には、ヘッダー情報としてxmin(作成トランザクションID)とxmax(削除・更新トランザクションID)が記録されています。

  • INSERT実行時:新しい行が追記され、その行のxminに現在のトランザクションIDが刻まれます(xmaxは0)。
  • DELETE実行時:物理的な行削除は行われず、既存行のxmaxに現在のトランザクションIDが刻まれて「論理削除」されます。
  • UPDATE実行時:既存行のxmaxが設定されて論理削除されると同時に、更新後のデータが新しい行としてページ内に追記されます。

過去に開始され現在も稼働している古いトランザクションが存在する場合、そのトランザクションからは「更新前の過去の行」が見えていなければなりません(一貫性のある読み取り)。そのため、更新前の古い行は即座にディスクから消去できず、誰からも参照されなくなるまで残り続けます。この「参照するトランザクションが全滅し、完全に不要となったゴミデータ」を不要タプル(Dead Tuple: デッドタプル)と呼びます。

1.3 デッドタプルが引き起こす2大弊害

不要タプルが掃除されずに蓄積し続けると、システムに以下の深刻な問題が発生します。

  1. テーブル肥大化(Table Bloat):実データが10万行しかないにもかかわらず、過去の更新ゴミが溜まり続けた結果、ディスク上では数百万行分の領域(数GB〜数十GB)を占有し続ける現象です。
  2. 全件走査(Seq Scan)やインデックススキャンの著しい低速化:データベースがディスクからデータを読み出す最小単位は「8KBのページ(ブロック)」です。不要タプルが混ざっていると、有効なデータを少し取得するだけでも大量の無駄なページをメモリへ読み込む必要が生じ、I/O負荷が跳ね上がります。

1.4 pg_stat_user_tables ビューで不要タプル数を可視化する

現在のデータベース内で、どのテーブルにどれだけの不要タプルが溜まっているかは、システム統計ビューpg_stat_user_tablesを参照することで正確に把握できます。

$ sudo -u postgres /usr/pgsql-18/bin/psql -d bench_db -c "
SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;"
  • n_live_tup:現在有効な行(Live Tuple)の推定件数。
  • n_dead_tup:まだ回収されていない不要タプル(Dead Tuple)の推定件数。
  • last_vacuum / last_autovacuum:直近で手動または自動バキュームが実行された日時。

1.5 行ヘッダ(xmin/xmax)と非表示システム列の実機解剖

不要タプルがどのようにディスク上に記録されているかを理解するため、PostgreSQLの内部システム列(Hidden System Attributes)を直接照会してみます。各行データには、ユーザー定義の列の前に23バイトの「行ヘッダ(HeapTupleHeader)」が付与されています。

主要な行ヘッダシステム列

  • ctid行の物理的な格納位置(ページ番号, ページ内のタプル番号)。例:(0, 1) は0番ページの1番目のスロット。
  • xminその行をディスクに挿入したトランザクションID(TXID)。
  • xmaxその行を削除または更新したトランザクションID。行が削除・更新されていない場合は 0

実際にテストテーブルを作成し、行を更新した際に行ヘッダがどのように変化するかを追跡します。

-- 1. テストテーブル作成と初期行の登録
CREATE TABLE bench_schema.row_demo (id int PRIMARY KEY, val text);
INSERT INTO bench_schema.row_demo VALUES (1, 'initial');

-- 初期状態のシステム列を確認
SELECT ctid, xmin, xmax, id, val FROM bench_schema.row_demo;
 ctid  |  xmin  | xmax | id |   val   
-------+--------+------+----+---------
 (0,1) | 734120 |    0 |  1 | initial
(1 行)

-- 2. 行を更新(UPDATE)
UPDATE bench_schema.row_demo SET val = 'updated' WHERE id = 1;

-- 更新後の状態を確認
SELECT ctid, xmin, xmax, id, val FROM bench_schema.row_demo;
 ctid  |  xmin  | xmax | id |   val   
-------+--------+------+----+---------
 (0,2) | 734121 |    0 |  1 | updated
(1 行)

通常のSELECTでは最新の有効行しか表示されませんが、物理位置 ctid(0, 1) から (0, 2) へ移動していることがわかります。古い行 (0, 1) はディスク上から即座に消去されたのではなく、xmax = 734121 が書き込まれた状態で「過去のトランザクションからは参照可能だが、現在のトランザクションからは見えない不要行(Dead Tuple)」としてファイル内に放置されています。この (0, 1) の領域を掃除して再利用可能にする処理こそが、VACUUMの役割です。

2. VACUUM と ANALYZE の本質的相違

「VACUUM」と「ANALYZE」は日常的に組み合わせて実行されるため混同されがちですが、その機能と目的は根本的に異なります。

2.1 VACUUM(通常バキューム)の役割

通常バキュームの主務は、「ページ内の不要タプルをクリーンアップし、空き領域マップ(FSM: Free Space Map)に登録して再利用可能にする」ことです。

不要タプルが占めていた領域を「空きスロット」として開放することで、次回以降に実行されるINSERTUPDATEがその空きスロットへ新しい行データを上書き配置できるようになります。これにより、テーブルの物理ファイルが外側へ膨張していくのを未然に防止します。

2.2 なぜ通常VACUUMを実行しても「OSのディスク容量」は減らないのか

インフラエンジニアから最も多く寄せられる疑問が、「大量データをDELETEした後にVACUUMを実行したのに、Linuxのdfコマンドで見ても空き容量が1バイトも増えない」という現象です。

その理由は、通常VACUUMは「ファイルの内部構造を再利用可能にする」だけであり、原則としてOSに対してディスク領域を返却しないためです。PostgreSQLがOSへ領域を返却できるのは、「テーブルファイルの最末尾のページ全体が完全に空っぽになった場合」に限られます。途中のページに空き領域ができても、ファイルサイズそのものは切り詰められません。開放された領域は「PostgreSQL内部の再利用枠」として保持されます。

2.3 ANALYZE:オプティマイザの頭脳となる統計情報の最新化

一方、ANALYZEコマンドは不要タプルの掃除を一切行いません。その役割は、「テーブル内のデータ分布を統計的にサンプリング調査し、システムカタログ(pg_statistic)を最新化する」ことです。

PostgreSQLのクエリプランナ(オプティマイザ)は、SQLが発行された際に「インデックスを使うべきか、全件走査(Seq Scan)すべきか、どのテーブルから結合すべきか」を判断するために統計情報を参照します。大量のデータ変更を行った後にANALYZEを怠ると、オプティマイザが過去の古いデータ分布を前提に劣悪な実行計画を選択し、システムの応答性能が突然数十倍に劣化する原因となります。

2.4 定常メンテナンスとしての VACUUM ANALYZE

実務で手動メンテナンスを行う際は、不要タプルの回収と統計情報の更新をワンセットで実施するVACUUM ANALYZEコマンドを実行するのが基本です。

-- 特定テーブルの不要タプル回収と統計情報更新を連続実行
$ sudo -u postgres /usr/pgsql-18/bin/psql -d bench_db -c "VACUUM ANALYZE bench_schema.items;"

3. なぜ本番環境で「VACUUM FULL」は危険とされるのか

テーブル肥大化(Bloat)が極限に達し、ディスク領域をOSへ物理的に返却したい場合、VACUUM FULLコマンドが存在します。しかし、本番稼働中の環境でVACUUM FULLを実行することは極めて危険であり、安易な実行は厳禁とされています。

通常VACUUM vs VACUUM FULL の挙動比較と autovacuum 判定式
図2: 通常VACUUM vs VACUUM FULL の挙動比較と autovacuum 判定式

3.1 危険な理由1:ACCESS EXCLUSIVE による読み書き全面停止

通常VACUUMが取得するロックはSHARE UPDATE EXCLUSIVEであり、アプリケーションからのSELECTINSERTUPDATEDELETEと完全に並行して実行できます(無停止運用が可能)。

対照的に、VACUUM FULLは最高レベルの排他ロックであるACCESS EXCLUSIVEを取得します。実行中は対象テーブルに対するデータの読み取り(SELECT)すら一切できなくなります。数GB〜数十GBのテーブルに対して実行した場合、数十分から数時間にわたってテーブルが完全凍結され、後続のWebトランザクションがすべてロック待ちとなってコネクションプールが枯渇し、システム全面停止を引き起こします。

3.2 危険な理由2:テーブル全件コピーによる一時ディスク容量の倍増

VACUUM FULLの内部処理は、既存ファイルをその場で縮めるのではなく、「有効なタプルだけを新しい物理ファイルへ丸ごとコピーして詰め直し、完了後に古いファイルを削除して差し替える」という動作をします。

したがって、処理を実行するには「対象テーブルのサイズと同等の空きディスク容量」が一時的に必須となります。ディスク使用率が90%に達して逼迫している危険な状況下で慌ててVACUUM FULLを発行すると、コピーの途中でディスク使用率が100%に到達し、データベース全体がクラッシュ停止する二次災害を招きます。

4. autovacuum(自動バキューム)の動作メカニズムとチューニング

現代のPostgreSQL運用では、手動でVACUUMを実行するのではなく、バックグラウンドで自律的に動作する「autovacuumデーモン」を正しく設定し、日常の保守を完全に自動化するのが鉄則です。

4.1 autovacuum デーモンの起動判定式

autovacuumデーモンは、テーブルの不要タプル数(n_dead_tup)が以下の計算式を満たした瞬間に、自動的にそのテーブルに対するVACUUMプロセスを起動します。

起動閾値 = autovacuum_vacuum_threshold + ( autovacuum_vacuum_scale_factor × n_live_tup )

PostgreSQLの標準パラメータでは以下のように設定されています。

  • autovacuum_vacuum_threshold = 50(固定下限行数:50行)
  • autovacuum_vacuum_scale_factor = 0.2(有効行数に対する比率:20%)

たとえば、有効行数が10,000行のテーブルの場合、50 + (0.2 × 10,000) = 2,050行の更新・削除が発生した時点でautovacuumが起動します。

4.2 大規模テーブルで発生する「自動実行の遅延問題」

デフォルトの係数(scale_factor = 0.2、つまり20%)は、小規模なテーブルでは健全に機能しますが、数千万行規模の大規模テーブルでは問題を引き起こします。

たとえば有効行数が1,000万行あるテーブルでは、20%にあたる「200万行」もの不要タプルが溜まるまでautovacuumが一度も起動しません。その結果、数GB単位の膨大な肥大化と性能劣化が進行してしまいます。

この問題に対処するため、更新頻度の高い大規模テーブルに対しては、個別テーブル単位で閾値を引き下げる設定(ストレージパラメータの変更)を行います。

-- 特定の大規模テーブルのみ、scale_factor を 5%(0.05)に引き下げて頻繁に自動清掃させる
ALTER TABLE bench_schema.items SET (
    autovacuum_vacuum_scale_factor = 0.05,
    autovacuum_vacuum_threshold = 1000
);

4.3 autovacuum の稼働状況を監視するログパラメータ

autovacuumが正常に動作しているか、処理に時間がかかりすぎていないかを把握するため、以下のパラメータを設定して実行ログを可視化します。

# autovacuum が 250ミリ秒以上かかった場合にログへ記録
log_autovacuum_min_duration = 250

5. 【実機検証】デッドタプル発生・回収とスロークエリログの採取

実機環境(alma-db01)を用いて、不要タプルの発生、手動VACUUMによる回収、およびスロークエリログの出力を検証します。

5.1 大量UPDATEによる不要タプルの生成

10,000行のデータを格納したitemsテーブルに対して全件更新を実行し、システム統計ビューpg_stat_user_tablesの挙動を確認します。

$ sudo -u postgres /usr/pgsql-18/bin/psql -d bench_db
bench_db=# UPDATE bench_schema.items SET price = price + 10;
UPDATE 10000

bench_db=# SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables WHERE relname = 'items';
 relname | n_live_tup | n_dead_tup 
---------+------------+------------
 items   |      10000 |      10000
(1 行)

有効データ10,000件に対して、更新前の古い行がn_dead_tup = 10000として確実に計上されていることが確認できます。

5.2 VACUUM (VERBOSE) による不要タプルの回収

手動で詳細モード(VERBOSE)のVACUUMを実行し、不要タプルがどのように回収されるかログを観察します。

実行手順

  1. VACUUM (VERBOSE) bench_schema.items;を実行します。
  2. ターミナルに出力される削除タプル数とページ情報を確認します。
  3. 統計ビューでn_dead_tupが0にリセットされたことを照合します。
bench_db=# VACUUM (VERBOSE) bench_schema.items;
INFO:  "bench_db.bench_schema.items"に対してVACUUMを実行しています
INFO:  テーブル"bench_db.bench_schema.items"のVACUUM完了: インデックススキャン: 1
ページ: 0削除、147残存、147スキャン (全体の100.00%)、0貪欲スキャン
タプル:10000削除、10000残存、0が削除されているがまだ除去できません
可視性マップ: 147ページが全可視 (all-visible)、73ページが全凍結 (all-frozen)
インデックススキャンが必要です: テーブル内の74ページにあった10000行の削除行識別子が削除されました
VACUUM

bench_db=# SELECT relname, n_live_tup, n_dead_tup, last_vacuum FROM pg_stat_user_tables WHERE relname = 'items';
 relname | n_live_tup | n_dead_tup |          last_vacuum          
---------+------------+------------+-------------------------------
 items   |      10000 |          0 | 2026-09-09 20:36:56.386346+09
(1 行)

「タプル:10000削除、10000残存」と記録され、10,000件の不要タプルが解放されて再利用可能な空きスロットへ変換されたことが実証されました。

5.3 スロークエリログ(log_min_duration_statement)の実動確認

postgresql.conflog_min_duration_statement = 100(100ミリ秒以上を記録)を設定した状態で、意図的に150ミリ秒かかるクエリを発行します。

-- 150ミリ秒スリープするテストクエリ
bench_db=# SELECT pg_sleep(0.15);

設定した閾値を超過したクエリの「実行時刻」「プロセスID」「ユーザー名」「DB名」「所要ミリ秒」「実行SQL全文」が漏れなく自動記録されました。このログを監視・集計することで、ボトルネックとなっている遅延クエリを日常的に特定できます。

5.4 HOT(Heap-Only Tuple)更新の最適化と fillfactor 設計

PostgreSQLの更新処理における最大の性能ボトルネックは、「行を更新するたびに、そのテーブルに定義されているすべてのインデックス(B-Tree等)にも新しい物理位置(ctid)へのポインタを追加登録しなければならない」という点です。1行のUPDATEに対して5個のインデックスがあれば、5箇所のインデックス更新I/Oが連鎖発生します。

このオーバーヘッドを解消するためにPostgreSQLに実装されているのが HOT(Heap-Only Tuple)更新 です。

HOT更新が成立する2大必須条件

  • インデックス非対象列の更新:更新対象の列が、いかなるインデックスのキーにも含まれていないこと。
  • 同一ページ内の空き領域:元の行が格納されている同一の8KBディスクページ内に、新しい行データを配置できるだけの空き容量(FSM)が存在すること。

HOTが成立すると、インデックス側には一切手を加えず、同一ページ内の旧タプルから新タプルへ直接内部ポインタを繋ぎます。これによりインデックスの肥大化が完全に防止され、更新処理のディスクI/Oが数分の一に激減します。頻繁にUPDATEされるテーブルでは、あらかじめ fillfactor(ページ充填率、デフォルト100%)を80%前後に引き下げ、ページ内に意図的に20%の空き領域を予約しておく設計が極めて有効です。

-- 頻繁に更新されるテーブルの fillfactor を 80% に調整
ALTER TABLE bench_schema.items SET (fillfactor = 80);
-- ※注意:ALTER TABLE による fillfactor 変更は新規追加・更新行にのみ適用されます。
-- 既存ページ全体を再配置して空き領域を確保するには、テーブル再構築(VACUUM FULL、CLUSTER、またはオンライン再構築ツール pg_repack)が必要です。

6. 実務トラブルシューティング事例集(3大障害シナリオと解決策)

本番運用現場でVACUUMおよび肥大化に関して最も頻発する3大トラブルと、その根本原因・対処法を解説します。

6.1 事例1:autovacuum が動作しているのに不要タプルが回収されない

「autovacuumは頻繁に実行ログが出ているのに、pg_stat_user_tablesn_dead_tup が全く減らずテーブルが肥大化し続ける」というトラブルです。

この原因の9割以上は、長時間放置されたトランザクション(Long-Running Transaction)、または 停止したまま放置されたレプリケーションスロット による「xmin地平線(xmin horizon)」の停止です。

xmin地平線(xmin horizon)によるVACUUM停止の仕組み

PostgreSQLのMVCCでは、「現在システム上で稼働している最も古いトランザクションの開始時点(oldest xmin)」よりも後に削除・更新された行は、その古いトランザクションから参照される可能性があるため、VACUUMであっても絶対に物理削除・回収することが許されません。たった1つのセッションが BEGIN; したままSELECTを発行して数時間放置されているだけで、システム全体の全テーブルで不要タプル回収が完全にブロックされます。

対処法として、以下のクエリで最古のトランザクションを特定し、不要なセッションを強制切断します。

-- 1. 最古のトランザクションを保持しているプロセスを特定
SELECT pid, usename, client_addr, state,
       backend_xmin, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY backend_xmin ASC LIMIT 1;

-- 2. 該当プロセスを安全に強制終了(特定したPIDが12345の場合の例示)
SELECT pg_terminate_backend(12345);

6.2 事例2:トランザクションID周回障害(TXID Wraparound)と緊急Freeze

PostgreSQLのトランザクションID(TXID)は32ビット符号なし整数(約42億通り)で管理されています。過去のトランザクションと将来のトランザクションを判別するため、PostgreSQLは循環する論理円を用いて「過去20億トランザクション前」を境界としています。

もし20億トランザクションを超えて一度もVACUUM FREEZE(過去行のTXIDを特殊なFrozenTransactionIdに書き換える作業)が行われないと、過去のデータが突然「将来発生したデータ」として扱われ、データが不可視(参照不能)となる重大障害が発生します。

これを防ぐため、データベースのトランザクション年齢(age)が autovacuum_freeze_max_age(デフォルト2億回)に達すると、PostgreSQLは通常の業務更新クエリを停止させてでも、全テーブルを強制的にVACUUM FREEZEする緊急モードに突入します。本番環境では、以下のクエリでDBの凍結年齢を定常監視し、緊急凍結が突発発生しないよう予防保守を行います。

-- データベース全体の凍結年齢監視(2億の緊急autovacuum閾値および20億の周回停止上限に対する進捗監視)
SELECT datname,
       age(datfrozenxid) AS freeze_age,
       round(age(datfrozenxid) * 100.0 / 200000000, 2) AS percent_towards_autovacuum,
       round(age(datfrozenxid) * 100.0 / 2000000000, 2) AS percent_towards_wraparound
FROM pg_database
ORDER BY freeze_age DESC;

6.3 事例3:テーブル肥大化(Bloat)のオンライン解消(pg_repackの活用)

すでに肥大化(Bloat)してしまい、OSの空きディスク容量を逼迫しているテーブルに対して、前述の通り VACUUM FULL はテーブル排他ロック(ACCESS EXCLUSIVE)を伴うため本番実行できません。

この実務課題を解決するデファクトスタンダードが、広く普及しているオープンソース拡張機能 pg_repack です。

pg_repack のオンライン再構築メカニズム

  • 元のテーブルと同じ構造の新規テーブルを裏で作成します。
  • 元のテーブルにトリガーを仕掛け、再構築中に発生した新データの変更差分(INSERT/UPDATE/DELETE)を一時ログテーブルに記録します。
  • 既存データを新テーブルへ全件コピーし、インデックスを再構築します。
  • 蓄積された差分ログを新テーブルへ適用して同期を完了させます。
  • 最後にシステムカタログ(pg_class)を一瞬(数ミリ秒の排他ロック)だけ切り替え、旧テーブルを削除します。

これにより、数億件のテーブルであっても業務クエリ(SELECT/INSERT/UPDATE/DELETE)を一切停止させることなく、物理サイズを最小限に圧縮してOSへ空き容量を完全返却できます。

7. 実務チートシート:日常保守・監視用SQL集

日々の運用保守現場で即座に実行できる診断用SQLスニペットをまとめます。

確認用途 実行SQLスニペット
不要タプル蓄積ワーストランキング SELECT relname, n_live_tup, n_dead_tup, round(n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;
最終VACUUM / ANALYZE日時確認 SELECT relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze FROM pg_stat_user_tables WHERE n_live_tup > 1000;
テーブルサイズ確認(実容量) SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS total_size, pg_size_pretty(pg_relation_size(relid)) AS table_size FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC;

7.1 稼働中VACUUMのリアルタイム進捗監視(pg_stat_progress_vacuum)

手動VACUUMやautovacuumが実行されている最中、「現在どのフェーズで、全体の何%まで完了しているのか」をリアルタイムで追跡するには、pg_stat_progress_vacuum ビューを活用します。

SELECT
    p.pid,
    c.relname AS table_name,
    p.phase,
    round(p.heap_blks_scanned * 100.0 / nullif(p.heap_blks_total, 0), 2) AS scan_pct,
    round(p.heap_blks_vacuumed * 100.0 / nullif(p.heap_blks_total, 0), 2) AS vacuum_pct,
    p.index_vacuum_count,
    now() - a.xact_start AS elapsed_time
FROM pg_stat_progress_vacuum p
JOIN pg_stat_activity a ON p.pid = a.pid
JOIN pg_class c ON p.relid = c.oid;

VACUUMは内部的に「走査(scanning heap)→ インデックス清掃(vacuuming indexes)→ ページ内タプル掃除(vacuuming heap)」というフェーズ遷移を行います。巨大テーブルのVACUUM中に進捗が不明で不安になる事態を防ぐため、このクエリを定常監視に組み込んでおくことが推奨されます。

7.2 無停止インデックス再構築(REINDEX CONCURRENTLY)の実務手順

大量のUPDATEを繰り返したテーブルでは、テーブルデータ本体だけでなく、B-Treeインデックス自体も不要なリーフノードが蓄積して肥大化(Index Bloat)します。インデックスが肥大化すると、キャッシュヒット率が低下して検索性能が悪化します。

通常の REINDEX TABLE は排他ロック(ACCESS EXCLUSIVE)を取得するため業務を停止させますが、PostgreSQL 12以降で強化された REINDEX TABLE CONCURRENTLY を用いることで、読み書きクエリを遮断することなくオンラインでインデックスを再構築できます。

-- 本番業務を止めずにインデックスを再構築(同時並行実行)
$ sudo -u postgres /usr/pgsql-18/bin/psql -d bench_db -c "
REINDEX TABLE CONCURRENTLY bench_schema.items;
"

内部的には新しいインデックスを並行作成し、作成完了後に古いインデックスとアトミックに差し替えるため、ロック待ちは発生しません(ただし処理時間と一時ディスク容量は通常REINDEXより多く消費されます)。

8. 理解度確認演習問題(全3問・解答解説付き)

第1問:MVCCアーキテクチャとVACUUMの役割

PostgreSQLのMVCCおよびVACUUMの動作に関する記述として、最も適切なものを選択してください。

  1. UPDATE文を実行すると、既存のディスクブロック内のデータが即座に新しい値で上書き保存される。
  2. 通常VACUUMを実行すると、回収された不要タプルの容量分だけ、直ちにOS上のテーブルファイルサイズが縮小してディスク空き容量が増加する。
  3. 通常VACUUMは不要タプルを掃除して空き領域マップ(FSM)を更新し、以降のINSERTやUPDATEで領域を再利用できるようにする。
  4. ANALYZEコマンドを実行すると、テーブル内の不要タプルがすべて物理的に削除される。
解答と解説を見る

正解:3

解説:通常VACUUMはページ内の不要タプルを回収してFSMに登録し、次のINSERTやUPDATEで空きスロットを再利用できるようにするのが主たる役割です。選択肢1は誤りでUPDATEはDELETE+INSERTとして動作します。選択肢2は誤りでOS空き容量は原則として増えません。選択肢4は誤りでANALYZEは統計情報の収集のみを行います。

第2問:VACUUM FULLの危険性

本番稼働中のシステムにおいて、手動でVACUUM FULLを実行することが極めて危険とされる主な理由の組み合わせとして、適切なものを選択してください。

  1. 対象テーブルに対する排他ロック(ACCESS EXCLUSIVE)を取得し、読み取りクエリ(SELECT)すら遮断してシステム停止を招くため。
  2. テーブル全件を新規ファイルへ再構築するため、元のテーブルサイズと同等の一時ディスク容量が必要となり、空き容量不足でクラッシュする恐れがあるため。
  3. 上記1と2の両方。
  4. VACUUM FULLを実行すると、テーブルのオプティマイザ統計情報が初期化されてインデックスが消去されるため。
解答と解説を見る

正解:3

解説:VACUUM FULLは対象テーブルに対する最高度の排他ロック(ACCESS EXCLUSIVE)を取得するため、処理完了まで全アクセスがブロックされます。さらに、新規ファイルへ全行を詰め直すコピー処理を行うため、元のテーブルと同等の空きディスク領域が必要となります。この2大リスクがあるため、本番運用中の実行は厳禁とされています。

第3問:autovacuumの起動条件計算

PostgreSQLのデフォルト設定(autovacuum_vacuum_threshold = 50autovacuum_vacuum_scale_factor = 0.2)において、有効行数(n_live_tup)が50,000行のテーブルが存在します。このテーブルに対して自動的にautovacuumが起動するデッドタプル数(n_dead_tup)として正しいものを選択してください。

  1. 50 行
  2. 1,050 行
  3. 10,050 行
  4. 50,000 行
解答と解説を見る

正解:3

解説:autovacuumの起動判定式はthreshold + (scale_factor × n_live_tup)です。数値を当てはめると、50 + (0.2 × 50,000) = 50 + 10,000 = 10,050行となります。したがって、更新や削除によって不要タプルが10,050件以上蓄積した段階で自動的にバキュームが開始されます。

9. まとめと次回予告

  • PostgreSQLのUPDATEは「旧行削除+新行追記」であるため、不要タプルの定期的な自浄作用が不可欠である。
  • 通常VACUUMはOSへディスク容量を返却しないが、ページ内を再利用可能にしてテーブルの無制限な肥大化を防ぐ。
  • VACUUM FULLはテーブル全面停止とディスク消費倍増の危険性があるため、計画停止時を除き本番環境での実行は避ける。
  • autovacuumは大規模テーブルで起動が遅れる傾向があるため、テーブル単位での閾値調整(scale_factor縮小)を検討する。
  • log_min_duration_statementを有効化し、性能低下の兆候を日常的に検知する運用基盤を整える。

次回は第7回「【無停止への第一歩】PostgreSQLのマイナーバージョンアップ実務:バイナリ更新と安全な切り戻し」を解説します。マイナー更新でデータ移行が不要となる理由、計画停止時間を最短化するセッションドレイン、dnf updateによる更新手順、そして万一の不具合に備えた事前バックアップからのロールバック設計を実機検証で整理します。

Kindle書籍版(第10章)のご案内:無停止テーブル再編成 pg_repack とXID周回対策

通常VACUUMでは一度肥大化してしまったテーブルのディスク容量をOSへ返却することはできません。商業出版予定のKindle書籍版『現場で困らない PostgreSQL 本番運用大全 —— 4ノード実機で作る高可用性・チューニング・障害復旧』第10章では、以下の高度な実務チューニングを網羅します。

  • pg_repack による無停止テーブル再編成:本番サービスを一切停止させず(排他ロックを回避し)、肥大化したテーブルとインデックスの空き領域を物理的にOSへ完全返却する必須ツールの導入・運用手順。
  • トランザクションID周回障害(XID Wraparound)の深層:放置するとデータベース全体が強制停止して書き込み不能に陥る「XID枯渇障害」のメカニズムと、フリーズ処理(vacuum_freeze_min_age)の緊急復旧手順。
  • pg_stat_statements によるボトルネックSQLの定量プロファイリング:システム全体の全クエリ呼び出し回数、総実行時間、キャッシュヒット率を可視化し、科学的なインデックス設計を行う手法。
前の記事
次の記事