【テクニカル・上級編】 カバリングインデックスとINCLUDE句 – PostgreSQL

なぜ我々は「ヒープアクセス」を恐れるのか —— カバリングインデックスとINCLUDE句の深淵

データベースエンジニアとして長く現場に立っていると、ふとした瞬間に「なぜこのクエリはこれほどまでに遅いのか?」という壁にぶつかることがあります。EXPLAIN ANALYZEを叩き、`Seq Scan`や`Bitmap Heap Scan`の文字を目にしたとき、多くのエンジニアは「インデックスが足りないんだな」と判断し、安直に複合インデックスを追加しようとします。

しかし、少し待ってください。闇雲にカラムを追加した巨大なインデックスは、書き込み負荷(Write Amplification)を増大させ、バッファキャッシュを無駄に圧迫する諸刃の剣です。

今日は、PostgreSQLにおける「真の最適化」の一つ、INCLUDE句を活用したカバリングインデックスについて、その内部構造と実戦的な使いどころを深掘りしてみましょう。

—

「インデックスオンリースキャン」の真価

PostgreSQLのクエリ最適化において、理想的な状態は「テーブルデータ(ヒープ)に一切触れずにクエリが完了すること」です。これがインデックスオンリースキャン(Index Only Scan)です。

通常、B-treeインデックスは「検索のためのキー(Key columns)」を保持しています。しかし、クエリがキー以外の列を要求すると、PostgreSQLはインデックスから得たポインタ(TID: Tuple Identifier)を頼りに、ヒープ(実テーブル)までデータを取りに行かなければなりません。

この「ヒープへの往復」こそが、ランダムI/Oを発生させ、パフォーマンスを劇的に落とす元凶です。特にテーブルのデータ量がメモリ上に乗り切らないサイズにまで肥大化している場合、このオーバーヘッドは致命的になります。

INCLUDE句が解決する「設計のジレンマ」

ここで登場するのが、PostgreSQL 11から導入された `INCLUDE` 句です。

例えば、`users`テーブルに対して「メールアドレスで検索し、かつユーザーのステータスを取得したい」というケースを考えてみましょう。

CREATE INDEX idx_users_email_include_status
ON users (email) INCLUDE (status);

このインデックスの重要な点は、`status`カラムが「検索キー」としては使われないという点です。B-treeの構造上、`status`はツリーのソート順には関与しません。あくまで「リーフノードに付随するおまけデータ」として格納されます。

なぜこれが賢い選択なのか?

1. ソートの複雑性を避ける: 複合インデックス `(email, status)` を作成すると、インデックスは `email` と `status` の両方でソートされます。もし検索条件が `email` だけの場合、クエリプランナーは柔軟な選択肢を持ちますが、インデックスの保守コストは高くなります。`INCLUDE` を使えば、B-treeの構造をシンプルに保ったまま、目的の列をリーフに忍び込ませることができます。
2. ユニーク制約との共存: `UNIQUE` インデックスを作成する際、`INCLUDE` を使えば、制約対象外のデータだけをリーフに持ち込めます。これにより、制約の整合性を守りつつ、カバーリングを実現するという離れ業が可能になります。

アーキテクチャの視点:なぜ「ヒープアクセス」が消えるのか

内部的な話をしましょう。PostgreSQLのB-treeインデックスのリーフノードには、各キーに対応するTIDが格納されています。`INCLUDE`句を使用すると、このリーフノードに「TIDとセットで、要求されたカラムの値」が物理的に配置されます。

クエリが実行される際、プランナーは「インデックスのリーフを見れば、必要なデータが全部揃っている」と判断します。その結果、ヒープへのアクセスを省略する実行計画が選択されるのです。

ただし、注意点があります。Visibility Map(VM)の存在です。
インデックスオンリースキャンが実際に発生するためには、そのインデックスが参照している行が「可視(Visible)」である必要があります。つまり、他のトランザクションから見て、そのデータが最新で確定していることをVMが証明できなければ、結局ヒープを見に行くことになります。

ですので、この手法を最大限に活かすには、定期的なVACUUMが不可欠です。VACUUMがサボっていると、せっかくのカバリングインデックスも「宝の持ち腐れ」になります。

現場で「勝つ」ためのヒント

最後に、現場でこのテクニックを適用する際のアドバイスを。

  • 過度な「INCLUDE」は禁物: なんでもかんでも含めればいいというわけではありません。インデックスサイズが肥大化すれば、それだけメモリ(shared_buffers)を食います。本当にそのクエリが多発し、かつパフォーマンスがボトルネックになっている場合のみに限定すべきです。
  • カーディナリティを意識する: 検索キー(`email`など)はカーディナリティが高いものを選び、`INCLUDE`には参照頻度の高い列を選ぶ。この分離こそが、インデックス設計の美学です。
  • EXPLAIN計画を信じろ: どんなに理論が正しくても、オプティマイザが「シーケンシャルスキャンの方が速い」と判断することもあります。統計情報が古いだけなのか、それともインデックスのコスト計算が適切なのか、EXPLAIN ANALYZEの結果をじっくり眺める時間を惜しまないでください。

データベースエンジニアリングの面白さは、こうした「物理的なデータの配置」と「論理的なクエリ」の間に橋をかけることにあります。`INCLUDE`句は、そのための強力なツールの一つに過ぎません。

さあ、あなたのデータベースのインデックスをもう一度見直してみませんか? 無駄なヒープアクセスが消えたとき、クエリのレスポンスタイムが劇的に改善する、あの瞬間の快感は格別ですよ。

コメント

タイトルとURLをコピーしました