肥大化したテーブルと「禁断の果実」:VACUUM FULLとREINDEXを正しく使いこなす
PostgreSQLを長く運用していると、必ず直面する壁がある。「なぜかストレージを食いつぶす」「インデックスが異常に肥大化し、スキャン速度が落ちた」という現象だ。
MVCC(多版同時実行制御)というPostgreSQLの設計思想上、更新の多いテーブルでデッドタプルが溜まるのは宿命だ。自動VACUUMが健気に働いていても、物理的な領域(ページ)が断片化し、インデックスがスカスカになることは避けられない。
今回は、そんな「溜まったツケ」を解消するための最終兵器、`VACUUM FULL`と`REINDEX`について、現場の視点から深く掘り下げていこう。
—
1. VACUUM FULLの正体と、その「重さ」
`VACUUM FULL`は、単なる掃除ではない。テーブルを物理的に新しい領域へコピーし、不要な領域を完全に切り捨てて再構成する「作り直し」だ。
なぜこれが危険なのか?
最大の理由は、排他的ロック(AccessExclusiveLock)にある。実行中は対象テーブルへの読み書きが完全に遮断される。本番環境の巨大なマスタテーブルでこれを叩こうものなら、即座にサービス停止だ。
もし、どうしても物理的に領域を詰めたいのであれば、現在は`pg_repack`という素晴らしいツールがある。これはトリガーと一時テーブルを使って、ロック時間を極限まで短縮しながらオンラインでテーブルを再構築する代物だ。私自身、大規模環境のメンテでは必ずこちらを第一選択にする。
—
2. インデックスの劣化とREINDEXの真実
インデックスは、更新されるたびに「インデックスページ」の断片化が起きる。特にUUIDのようなランダム性の高いキーを使っている場合、B-treeの構造はあっという間にスカスカになり、検索効率が劇的に悪化する。
REINDEX CONCURRENTLYという福音
かつて、インデックスの再構築はテーブル同様に重い処理だった。しかし、PostgreSQL 12で導入された `REINDEX CONCURRENTLY` は、まさにゲームチェンジャーだ。
- 挙動の仕組み: まず新しいインデックスを作成し、その後、元のインデックスと切り替える。
- メリット: インデックス構築中に読み書きをブロックしない。
ただし、注意点がある。`CONCURRENTLY`は「インデックスの構築」という重いCPU/I/O処理をバックグラウンドで行うため、システム全体の負荷は跳ね上がる。監視なしに安易に実行すると、DB全体のレスポンスが壊滅する可能性がある。実行時は、必ずシステムのピークタイムを避けるのが鉄則だ。
—
3. パフォーマンストラブルシューティングの勘所
もしDBが重いと感じたら、以下の手順で「インデックスが原因か」を切り分けてほしい。
1. `pg_stat_user_indexes`を確認する
`idx_scan`と`idx_tup_fetch`の比率を見れば、インデックスがどれだけ有効に使われているか、あるいはどれだけ肥大化して無駄なI/Oを生んでいるかが透けて見える。
2. `pgstattuple`拡張を使う
`pgstattuple`関数を使えば、テーブルやインデックスの「死んでいるタプル(Dead Tuple)」の割合や、インデックスの「断片化率(fillfactorの隙間)」が数値として可視化できる。直感ではなく、数値で「再構築が必要か」を判断するんだ。
—
最後に:メンテの極意
「とりあえずVACUUM FULL」は、外科手術で例えるなら「患部を切除するために全身麻酔をかける」ようなものだ。
- 物理的な空き領域が必要なのか?
- それともインデックスの深さが問題なのか?
- そもそも、fillfactorを調整して、更新の頻度に合わせてページに余白を持たせることで再構築の頻度を下げられないか?
PostgreSQLは非常に奥が深い。闇雲にコマンドを打つのではなく、アーキテクチャを理解し、ボトルネックを正確に特定する。それができるエンジニアこそが、データベースの性能を極限まで引き出せるんだ。
さて、あなたのDBは今日も軽快に動いているだろうか?ログを眺めるその目が、今日は少しだけ鋭くなることを期待している。
コメント