TL

ガベージコレクション:MVCC バージョンのvacuumとpurge

テーブルが原因不明に膨らむ謎を、不要バージョンを回収する仕組みから解けます。PostgreSQL の VACUUM・HOT と InnoDB の purge スレッドの判断基準を押さえれば、長時間トランザクションが招く膨張を予防できます。

応用データベースMVCCVACUUMPostgreSQLInnoDBガベージコレクション最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.MVCC は古い版を即座には消さず、誰からも見えなくなった版を後追いで回収する。回収可能ラインは最古の読み手が辿りうる位置で決まる。
  • 2.PostgreSQL は VACUUM が死んだタプルを再利用可能化し、HOT で更新を局所化、freeze で XID 周回を防ぐ。InnoDB は purge スレッドが不要 undo を非同期解放する。
  • 3.長時間トランザクションは回収可能ラインを過去に固定し、bloat や undo 膨張・purge 遅延を招く。短いトランザクションが唯一の根本対策。

なぜ「回収」が独立した問題になるのか

MVCC は更新を上書きではなく新しい版の追加で表現します(→ MVCC の内部実装)。この設計の必然的な代償が、誰からも見えなくなった古い版が物理的に残り続けることです。これを片付けるのが MVCC のガベージコレクション、すなわち PostgreSQL の VACUUM と InnoDB の purge です。ここではその回収器の内部、いつ・何を・どの基準で消すのかを掘り下げます。

回収の中核は1つの問いに尽きます。「この版は、現在および将来の読み手の誰からも二度と参照されないか」。一度でも参照しうる読み手が残っていれば消せません。この境界を PostgreSQL では xmin horizon、InnoDB では read view の最古点と呼びます。回収器の賢さも限界も、この境界の計算精度で決まります。

回収可能ラインの計算

回収できるのは、削除(または旧版化)した XID が「全ての生きたスナップショットから見てコミット済み」になった版だけです。判定の擬似コードは次のとおりです。

removable(dead_version):
  killer = dead_version を消した XID
  # killer が、現存する最古スナップショットより前にコミット済みなら
  # どの読み手にとっても「既に消えた版」= 回収可能
  return committed_before(killer, oldest_live_snapshot)

ここで oldest_live_snapshot(最古の生きたスナップショット)が鍵です。PostgreSQL はクラスタ全体で最も古い実行中トランザクションの xmin を集計し、それより前に消えた死タプルだけを回収対象とします。hot_standby_feedback を有効にしたレプリカや、準備済みの2相コミットトランザクションもこの境界を後ろに引っ張ります。InnoDB も同様に、最古の read view が参照しうる undo より新しいものは解放しません。

回収は版そのものではなく「境界」で決まる

どれだけ死んだ版が積み上がっても、最古の読み手が前進すれば一気に回収可能になります。逆に死んだ版が1件でも、最古の読み手が固まっていれば永遠に回収できません。問題は版の量ではなく境界の位置、という視点が運用判断の軸になります。

PostgreSQL:VACUUM・HOT・freeze の三層

PostgreSQL の回収は性格の異なる3つの仕事を兼ねます。

仕事対象効果
死タプル回収xmin horizon より古い dead tuple領域を再利用可能化し bloat を抑制
インデックス整理死タプルを指す不要なインデックスエントリインデックス側の肥大化を解消
freeze十分に古い生きたタプルの XIDXID 周回(wraparound)を防止

通常の VACUUM はファイルを OS に返さず、死タプルが占めた領域を同じテーブル内の空きリストに載せ、次の挿入や更新へ回します。ファイルを実際に縮めるには VACUUM FULL(テーブル全体を書き直し、排他ロックを取る)が必要で、運用では避けたい操作です。

autovacuum はテーブルごとの死タプル推定数がしきい値を超えると起動します。おおまかな式は次のとおりです。

threshold = autovacuum_vacuum_threshold
          + autovacuum_vacuum_scale_factor * 行数
死タプル数 > threshold なら autovacuum を起動

scale factor の既定は 0.2 なので、大きなテーブルほど起動が遅れ、bloat が表面化しやすくなります。巨大テーブルでは scale factor を個別に小さくするのが定石です。

HOT:そもそも版チェーンを短くする

HOT(Heap-Only Tuple)は更新がインデックス対象列を変えず、かつ同一ページ内に新版を置ける場合の最適化です。新版はインデックスから直接は指されず、旧版からページ内のポインタで辿られます。これによりインデックス更新を省け、さらに通常の SELECT 時にも不要になった中間版をその場で外す HOT pruning(軽量な版回収)が働きます。HOT が効くかどうかで bloat の進み方が大きく変わるため、頻繁に更新される列はインデックスから外す設計が有効です(→ B-Tree の内部構造)。

freeze:有限な XID を守る

XID は約42億で一周する有限値です。古い行の XID を放置すると、周回した新しい XID が「未来」と誤認され可視性判定が壊れます。これを防ぐため VACUUM は十分に古い行を「凍結(freeze)」し、常に見える版として印を付けます。

wraparound は最終的に書き込みを止める

freeze が間に合わず周回の危険水位に達すると、PostgreSQL はデータ破損を避けるため強制的に anti-wraparound VACUUM を走らせ、最悪の場合は新規トランザクションの受付を停止します。大量更新を続けるシステムで autovacuum を止めるのは禁忌です。age(relfrozenxid) の監視が予防になります。

InnoDB:purge スレッドと history list

InnoDB は本体(クラスタ化インデックス)に最新版だけを置き、旧版を undo ログから再構成する方式です(→ ロックと MVCC)。回収対象は本体の死タプルではなく、不要になった undo ログレコードで、これを専任の purge スレッドが非同期に処理します。

purge の仕事は2つです。第一に、どの read view からも参照されなくなった undo レコードを解放すること。第二に、削除マーク付きのレコードを本体とセカンダリインデックスから実際に物理削除すること(InnoDB の DELETE はまず削除マークを付け、purge が後で実体を消す遅延削除です)。

未解放の undo の長さは history list length という指標に表れ、SHOW ENGINE INNODB STATUS で確認できます。これが増え続けるのは purge が最古 read view に阻まれて前進できないサインです。

観点PostgreSQL(VACUUM)InnoDB(purge)
膨らむ場所テーブル/インデックス本体(bloat)undo テーブルスペース
実行主体autovacuum / 手動 VACUUMpurge スレッド(既定で複数)
遅延の指標回収できない死タプル数・age(relfrozenxid)history list length
削除の扱いdead tuple として回収削除マーク後に物理削除(遅延削除)
固有の責務XID freeze による wraparound 防止セカンダリの purge も担当

長時間トランザクションが回収を止める

両エンジンに共通する最大の落とし穴が、長時間トランザクションです。読みっぱなしの BEGIN が1本でもあると最古スナップショットがそこに固定され、回収可能ラインが前進しません。

アイドルな開きっぱなしが最悪

最も危険なのは BEGIN 後に何もせず開いたままのアイドルトランザクションです。本人は1行も読んでいなくても、回収器は「将来古い版を参照するかもしれない」と判断し続けます。結果、PostgreSQL では死タプルが回収されず bloat が進行し、InnoDB では undo が解放されず history list length が膨張し、purge 全体が停滞します。PostgreSQL は idle_in_transaction_session_timeout で、アプリ側は接続プールのトランザクション境界の見直しで対処します(→ トランザクション分離レベル)。

兆候の見つけ方を押さえておきます。PostgreSQL では pg_stat_activity の古い xact_startbackend_xmin、肥大化したテーブルの dead tuple 数。InnoDB では history list length の継続的な増加。どちらも「最古の読み手は誰か」を突き止め、それを終わらせれば回収が一気に進みます。

まとめ

  • MVCC の回収器が消せるのは「最古の読み手から見て確実に消えた版」だけで、回収の成否は版の量ではなく回収可能ラインの位置で決まる。
  • PostgreSQL の VACUUM は死タプル回収・インデックス整理・freeze による wraparound 防止の三役を担い、HOT が版チェーンの伸びそのものを抑える。
  • InnoDB の purge スレッドは不要 undo の解放と削除マークの遅延物理削除を非同期に行い、停滞は history list length に表れる。
  • 両者に共通する根本対策は、最古スナップショットを固定する長時間・アイドルトランザクションを排除すること。直列化可能性との関係は 直列化可能スナップショット分離 も参照。

データベース Article

ガベージコレクション:MVCC バージョンのvacuumとpurgeを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

データベース

比較で見る軸

難易度: advanced / カテゴリ: データベース / タグ数: 6

導入後に効く点

PostgreSQL は VACUUM が死んだタプルを再利用可能化し、HOT で更新を局所化、freeze で XID 周回を防ぐ。InnoDB は purge スレッドが不要 undo を非同期解放する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
データベース
タグ数
6

判断チェックリスト

  • 自社の用途が「データベース / MVCC」に近いか確認する。
  • 強みである「MVCC は古い版を即座には消さず、誰からも見えなくなった版を後追いで回収する。回収可能ラインは最古の読み手が辿りうる位置で決まる。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

データベースMVCCVACUUMPostgreSQLInnoDBデータベースMVCCVACUUM
参考: 公式情報