はじめに
インデックスは効いているはずなのに、思ったより速くない。そんな経験はないでしょうか。
その一因が、インデックス検索のあとに走る「テーブル本体アクセス」です。実はこの追加ページ読み込みを丸ごと省略する書き方があります。それがカバリングインデックス(Covering Index)です。
この記事では、通常のインデックス検索が裏で何をしているかを整理してから、カバリングインデックスの仕組み・使いどころ・トレードオフを順に見ていきます。
動く図解で確認したい方は、こちらのサイトも合わせてどうぞ
通常のインデックス検索は 2 段階
INDEX(name) に対して次のクエリを投げる場面を考えます。
SELECT email FROM users WHERE name = 'Sato';
裏で DB が実行するのは、大きく分けて 2 ステップです。
ステップ 1: インデックス探索
B-tree をたどって name = 'Sato' の葉ノードに到達し、そこに記録されている行 ID(PostgreSQL では CTID、MySQL では ROWID)を取り出します。
ステップ 2: テーブル本体のページ読み込み
取り出した行 ID を頼りに、テーブル本体の該当ページを読み、email カラムの値を取り出します。
[インデックス] [テーブル本体]
name | 行ID page 1: [row0, row1, row2, ...]
------|------ page 2: [row3, row4, row5, ...]
Sato | (1, 0) ─────→ ↑ ここを追加で読む
Sato | (2, 0) ─────→ ↑
Suzuki| (1, 1) ...
ステップ 2 のテーブルアクセスは、該当行が多いほど支配的な時間になります。「インデックスは効いているのに遅い」の典型パターンです。
カバリングインデックス = ステップ 2 を省略する
必要なカラムをインデックス側に全部含めておくと、テーブル本体を触らずに結果を返せます。これがカバリングインデックスの発想です。
-- PostgreSQL / SQL Server の書き方
CREATE INDEX idx_users_name_covering
ON users (name) INCLUDE (email);
SELECT email FROM users WHERE name = 'Sato';
-- → インデックスの中に email があるので、テーブル本体を読まずに返せる
INCLUDE は「キーには使わないが、インデックスに一緒に持たせておく」カラムを指定するオプションです。ソートには参加しませんが、リーフに値が同居するので取り出すだけなら十分。
MySQL には INCLUDE 構文がないので、素直な複合インデックスで同じ効果を得ます。
-- MySQL の書き方
CREATE INDEX idx_users_name_covering
ON users (name, email);
SELECT email FROM users WHERE name = 'Sato';
-- → 複合インデックスの葉に email があるので、テーブル本体を読まない
(name, email) は name の等価検索でも普通に効くので、副作用も少ないです。
実行計画で見分ける
カバリングが効いているかどうかは、実行計画に**「テーブル本体を触っていない」**ことを示す文言があるかで判定できます。
PostgreSQL
Index Only Scan using idx_users_name_covering on users
Index Only Scan が出ていればカバリング成功です。
MySQL
| Extra |
+-----------------+
| Using index |
Using index が出ていればカバリング成功。逆に Using where; Using index condition などが出るときはテーブル本体を読んでいます(インデックス条件のみ push down)。
トレードオフを忘れずに
「これで全部カバーすればいいじゃん」とはなりません。
- インデックスサイズが増える: 追加カラムの分だけディスクを使う
- 書き込みコストが増える: INSERT/UPDATE でカバー対象のカラムを更新するたび、インデックス側も更新が走る
-
大きなカラムを含めるとインデックスが膨らむ:
TEXTや大きなVARCHARは要注意
なので設計の原則はシンプルです。
- 頻出クエリだけをカバーする(全 SELECT をカバーしようとしない)
-
SELECT *を避けて必要な列だけを返す(そもそもカバリング設計しやすくなる) - ソートや集約に使うカラムを含めるとインデックスから直接結果が返せる
PostgreSQL の場合、INCLUDE を使えばキーの並び順を壊さずに追加カラムを持たせられるので、既存インデックスに継ぎ足す形で試しやすいです。
まとめ
- 通常のインデックス検索は「インデックス探索 → テーブル本体読み込み」の 2 段階
- カバリングインデックスは 2 段階目を省略する(必要なカラムをインデックス側に持たせる)
- PostgreSQL は
INCLUDE、MySQL は(key, extra)の複合インデックスで実現 - 実行計画に
Index Only Scan/Using indexが出れば成功 - サイズ・書き込みコストとのトレードオフがあるので、頻出クエリだけに絞って設計する
「インデックスは効いているのに遅い」ときに、まず疑うべき候補として覚えておくと役立ちます。
インデックスの内部構造・カラム順・カバリングの効き方などを動く図で確認できるサイトを作っています。気になる方は覗いてみてください。
バグ・ご意見は X @taitech_dev まで。