【実務・中級編】 カバリングインデックスとINCLUDE句 – PostgreSQL

「インデックスはあるのに遅い…」を解決する、INCLUDE句という魔法の切り札

現場で運用しているデータベース、時々「これだけインデックスを張っているのに、なんでクエリが遅いんだ?」と首をかしげたくなること、ありますよね。

実行計画(`EXPLAIN ANALYZE`)を見ると、せっかくインデックスがあるのに「Heap Fetches」が多発している……。つまり、インデックスで当たりをつけてから、わざわざ本体のテーブル(ヒープ)までデータを取りに行っている状態です。これ、いわゆる「テーブルアクセス」がボトルネックになっている典型的なパターンです。

今回は、そんな状況を打破するスマートな解決策、PostgreSQLの「INCLUDE句」について、現場の知見を交えて解説します。

—

カバリングインデックスとは何か?

まず前提の整理です。PostgreSQLにおいて「インデックスオンリースキャン(Index Only Scan)」を実現するには、クエリが必要とするすべての列が、インデックスの中に含まれている必要があります。これが「カバリングインデックス」です。

例えば、`users`テーブルからメールアドレスを検索したいとき。

SELECT email FROM users WHERE user_id = 123;

`user_id`だけにインデックスを張っても、`email`を取得するために結局テーブルを見に行かなければなりません。じゃあ、`CREATE INDEX idx_user_id_email ON users(user_id, email);` とすればいいじゃないか、と思うかもしれません。

確かにこれならインデックスオンリースキャンは効きます。しかし、ここには「B-treeインデックスの特性」という罠があるんです。

—

なぜ `INCLUDE` 句を使うのか?

B-treeインデックスは、キー(インデックス列)の順序を保持してソートされています。そのため、列をたくさん追加するとインデックス自体のサイズが肥大化しますし、メンテナンスコストも馬鹿になりません。

ここで登場するのが `INCLUDE` 句 です。

CREATE INDEX idx_user_id_include_email
ON users (user_id)
INCLUDE (email);

この書き方をすると、PostgreSQLはこう考えます。
「`user_id`はソート順を維持して検索用キーとして使うけど、`email`はソート順には関与させないよ。単にインデックスの末尾に『おまけ』としてくっつけておくだけにするね」と。

INCLUDE句を使うメリット:

1. 検索性能は維持しつつ、サイズを抑制できる:ソート対象にならない分、インデックスの構造がシンプルになります。
2. インデックスオンリースキャンの恩恵をフル活用できる:クエリ実行時、`email`を取りに行くためにテーブルへ行く必要がなくなり、インデックス内だけで完結します。

—

実践:こんなシチュエーションで使おう

例えば、ECサイトの注文履歴(`orders`テーブル)で考えてみましょう。

— よくあるクエリ
SELECT order_date, total_amount
FROM orders
WHERE user_id = 999
ORDER BY order_date DESC;

この場合、`user_id`と`order_date`は検索とソートに使うので、そのままインデックスのキーにします。でも、`total_amount`はただ取得したいだけですよね。

CREATE INDEX idx_orders_user_date_include_amount
ON orders (user_id, order_date DESC)
INCLUDE (total_amount);

こうしておけば、`user_id`で絞り込み、`order_date`で並び替え、その過程で`total_amount`までインデックスから拾い出せる。テーブルへのランダムアクセスを極限まで減らせるため、体感速度が劇的に変わります。

—

注意点:魔法じゃない、使いどころは選ぶ

とはいえ、何でもかんでも `INCLUDE` すればいいというわけではありません。

  • 更新頻度には要注意:`INCLUDE`した列が頻繁に更新されると、そのたびにインデックスの更新も走ります。書き込み負荷が高いテーブルでは、インデックスの肥大化と更新コストのバランスを慎重に見極める必要があります。
  • ディスク容量とのトレードオフ:あくまで「読み取りを爆速にするための武装」です。あまりに多くの列を詰め込みすぎると、今度はメモリ(`shared_buffers`)を圧迫してしまいます。

—

先輩からのアドバイス

実務でパフォーマンスチューニングをするときは、まず「なぜテーブルアクセスが発生しているのか?」を突き止めることが第一歩です。`EXPLAIN`のログを見て、Heap Fetchesが多ければ、迷わずこの `INCLUDE` 句を試してみてください。

「インデックスを張ったのに遅い」という悩みから卒業できると、データベースの挙動が手に取るように見えるようになって、チューニングが一段と楽しくなりますよ。

それでは、また現場でお会いしましょう!

コメント

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