インデックスの「枯れ」と向き合う:REINDEX CONCURRENTLYという選択肢
データベースの運用を長く続けていると、避けられないのが「インデックスの肥大化と断片化」です。特に更新頻度の高いテーブルでは、MVCCの影響でデッドタプルが蓄積し、B-treeのページがスカスカになることは珍しくありません。
「クエリがなんとなく遅い」。実行計画を見るとインデックススキャンは選ばれているのに、なぜかコストが高い。そんなとき、私たちはインデックスの再構築というメンテナンスを検討します。しかし、本番環境で `REINDEX TABLE` を叩くのは、ある種の「爆弾」を投げるようなものです。
かつて、私たちはこのメンテナンスのためにメンテナンス時間を確保し、テーブルを排他ロックし、ユーザーからの悲鳴を覚悟して実行していました。しかし、PostgreSQL 12で導入された `REINDEX CONCURRENTLY` は、その景色を一変させました。今日は、この機能の裏側と、運用における「賢い付き合い方」について話をしましょう。
—
魔法ではない、段階的なインデックス構築
`REINDEX CONCURRENTLY` がなぜロックを解放できるのか。それは、内部的に非常に洗練された「段階的な構築プロセス」を踏んでいるからです。
通常の `REINDEX` が既存のインデックスを直接書き換えるのに対し、`CONCURRENTLY` を付与すると、PostgreSQLは以下の手順を踏みます。
1. 新しいインデックスの作成: まず、既存のものとは別に、新しいインデックスを「無効」な状態で作成します。
2. スキャンと構築: テーブルを全スキャンし、新しいインデックスを構築します。この間、既存のインデックスはそのまま動き続けます。
3. トランザクション待ち: ここが肝です。構築中に行われた更新操作を新しいインデックスにも反映させるため、さらに二度のスキャンが行われます。
4. 切り替え: 最後に、新しいインデックスを「有効」に切り替え、古いインデックスを削除します。
この仕組みにより、テーブルに対する `SHARE UPDATE EXCLUSIVE` ロック(読み取りを阻害しない程度の軽いロック)で済ませることが可能になります。
—
運用でハマる「落とし穴」への処方箋
「じゃあ、明日から全部これでメンテナンスしよう」と思ったあなた、少し待ってください。この手法には、いくつかの考慮すべきコストが存在します。
1. I/O負荷の増大
`CONCURRENTLY` は、実質的に二重のインデックス構築を行っているようなものです。当然、ディスクI/OとCPU負荷は跳ね上がります。ピーク時に実行すれば、メインのクエリを圧迫して本末転倒な状況になりかねません。バッチ処理の合間や、トラフィックの谷間を狙うのが定石です。
2. 「無効なインデックス」というリスク
時折、ネットワークの瞬断やデッドロックが原因で、構築が完了できずに途中で止まってしまうことがあります。このとき、データベース内には「無効なインデックス」が残ります。
`pg_index` システムカタログを確認し、`indisvalid` が `false` になっているインデックスがないか、定期的に監視するスクリプトを走らせておくことは、熟練エンジニアの最低限の礼儀です。
3. トランザクションの競合
`REINDEX CONCURRENTLY` は、構築の最終段階で、対象テーブルにアクセスしているすべてのトランザクションが終了するのを待ちます。もし、実行時間の長い(ロングトランザクション)クエリが裏で走っていると、いつまでも処理が完了せず、その後の処理が詰まる原因となります。
—
パフォーマンスチューニングのその先へ
私が現場でよくアドバイスするのは、「`REINDEX` をする前に、まずは `pgstattuple` でインデックスの有効利用率を確認すること」です。
— インデックスの断片化率を調査する
SELECT FROM pgstatindex(‘your_index_name’);
`avg_leaf_density` が極端に低い場合は再構築の価値がありますが、そうでない場合は、単なるインデックスの統計情報の更新(`ANALYZE`)で解決することもあります。
`REINDEX CONCURRENTLY` は強力な武器ですが、闇雲に使うものではありません。アーキテクチャを理解し、現在のシステム負荷と天秤にかけ、適切なタイミングで「外科手術」を施す。それこそが、PostgreSQLを極めるエンジニアの嗜みではないでしょうか。
皆さんのデータベースが、今日も軽快にクエリを捌いていることを願っています。また別の技術トピックでお会いしましょう。
コメント