「インデックスの肥大化、そのまま放置してない?」
DBエンジニアとして現場を回っていると、よくこんな相談を受けます。「最近、なんだかクエリが遅い気がする」「インデックスは貼ってあるのに、フルスキャンに近い動きをしてる」。
データ更新が頻繁なテーブルを使っているなら、それはインデックスの肥大化(Bloat)が原因かもしれません。PostgreSQLを使っているなら、メンテナンスは避けて通れない道。でも、本番環境でテーブルをロックして「数分間サービス停止」なんて、今の時代、許されないですよね。
そこで今回は、運用中の強い味方、`REINDEX CONCURRENTLY`について、現場の知見を交えて深掘りしてみます。
—
なぜインデックスは「太る」のか?
PostgreSQLのインデックスは、MVCC(多版型同時実行制御)の仕組み上、削除や更新が行われると、古いデータへの参照がインデックス内にゴミとして残ってしまうことがあります。これが積み重なると、インデックスのサイズが物理的に巨大化し、読み込み効率がガクンと落ちるわけです。
昔ながらの `REINDEX TABLE` コマンドは強力ですが、作業中は対象テーブルに強力なロック(AccessExclusiveLock)をかけます。 つまり、その間はSELECTすらできなくなる。これ、本番環境でやったら目も当てられませんよね。
—
救世主:REINDEX CONCURRENTLY
PostgreSQL 12から標準搭載された `REINDEX CONCURRENTLY` は、その名の通り「並行して」インデックスを再構築するコマンドです。
仕組みをざっくり理解する
このコマンドが何をしているかというと、実は裏側でこんなステップを踏んでいます。
1. 新しいインデックスの作成開始:元のインデックスと並行して、新しいインデックスを「作成中」ステータスで構築します。
2. データの同期:並行して行われる書き込み操作を、新しいインデックスにも反映し続けます。
3. 切り替え:古いインデックスを捨て、新しいものに差し替えます。
この間、テーブルへの読み書きは完全に許可されます。これが「ノンブロッキング」の魔法です。
基本の使い方
使い方はシンプルです。
— テーブル全体のインデックスを再構築する場合
REINDEX TABLE CONCURRENTLY users;
— 特定のインデックスだけを対象にする場合
REINDEX INDEX CONCURRENTLY users_email_idx;
—
現場で使う時の「注意点」と「コツ」
「じゃあ明日から全部これに変えよう!」……ちょっと待ってください。いくつか現場でハマりやすいポイントがあるんです。
1. 完了までに時間がかかる
通常のREINDEXよりも、新しいインデックスを構築して同期を維持する分、処理時間は長くなります。リソース(CPUやI/O)もそれなりに食うので、ピークタイムを避けるのは鉄則です。
2. 「失敗」の可能性がある
これが一番の落とし穴。`CONCURRENTLY` は、再構築中にインデックスの競合やデッドロックが発生すると、途中で失敗することがあります。
失敗すると、「INVALID(無効)」なインデックスが残ります。 これが残ったままだとDBのパフォーマンスが正しく出ないので、必ず `DROP INDEX` で消して、再度やり直す必要があります。
3. トランザクション内では使えない
`REINDEX CONCURRENTLY` は、トランザクションブロック(`BEGIN` 〜 `COMMIT`)の中で実行できません。スクリプトに組み込む際は注意してください。
—
最後に:運用の心構え
僕が現場で運用する際は、必ず 「監視」と「計画」 をセットにします。
- 実行タイミングの選定:ZabbixやDatadogのメトリクスを見て、I/O負荷が低い時間帯を狙う。
- 実行後の確認:コマンドを打って「はい終わり」ではなく、`pg_stat_user_indexes` を見て、インデックスのサイズが小さくなったか、`invalid` なインデックスが残っていないかを確認するまでがメンテナンスです。
「とりあえず再構築すれば速くなるはず」という思考停止は危険です。まずは `pgstattuple` 拡張などを使って、どれくらい肥大化しているかを数値で確認することから始めてみてください。
技術は正しく使えば、最強の武器になります。インデックスのメンテナンスも、怖がらずに、でも慎重に。これだけで、DBのパフォーマンスは劇的に安定しますよ。
それでは、良いDBライフを!
コメント