カバリングインデックスでテーブルアクセスを省く(Index Only Scan)

通常のインデックススキャンには「テーブル本体への各行ごとのアクセス」がついてまわり、これがパフォーマンス劣化の主因になりがちです。カバリングインデックス(インデックスのみのスキャン) はこのテーブルアクセスを完全に省略する技法で、Markus Winand は「あらゆるチューニング法の中でも最強の方法のひとつ」と評しています。

ただし「欲張って列を増やすとアンチパターンになる」という側面もあり、設計には注意点があります。

仕組み

通常のインデックスアクセスは以下の流れです(B-tree内部構造の解説 も参照)。

  1. ツリー走査でリーフノードに到達
  2. リーフノードチェーンを舐めて該当エントリを集める
  3. エントリに含まれる ROWID をもとにテーブル本体にアクセスして全列を取得

3 のテーブルアクセスを TABLE ACCESS BY INDEX ROWID といい、対象行が多くなるほど I/O コストが線形に増えていきます。

ここで SELECT 句で参照される全ての列がインデックスに含まれていれば、3 のステップが不要になります。これを インデックスのみのスキャン、またはインデックスがクエリ全体を「カバー」するので カバリングインデックス と呼びます。

-- subsidiary_id と eur_value をインデックスに含める
CREATE INDEX sales_sub_eur ON sales(subsidiary_id, eur_value);

-- SELECT は eur_value だけ、WHERE は subsidiary_id だけ
-- 両方ともインデックスに含まれているのでテーブルアクセス不要
SELECT SUM(eur_value) FROM sales WHERE subsidiary_id = ?;

効果

クラスタ化係数が悪いインデックス(インデックス順とテーブル順が一致しない、対象行が広いブロック数に散らばる)ほど効果が大きくなります。

著者 Markus Winand のサンプルでは、4 万行を SUM するケースでカバリングを成立させると 4 万回のテーブルアクセスを省略 できます。書籍中で「あらゆるチューニング法の中でも最強の方法のひとつ」と評価されているのはこの効果による発言です。

各 RDBMS での確認方法

カバリングインデックスが成立しているかは EXPLAIN で確認できます(EXPLAIN の読み方 も参照)。

RDBMS表示
MySQLExtra: Using index
PostgreSQLIndex Only Scan
SQL Server実行計画で Key Lookup / RID Lookup が 無い こと
OracleINDEX FAST FULL SCAN または TABLE ACCESS BY INDEX ROWID が無いこと

注意点: WHERE 列を 1 つ足すだけでカバリングが崩れる

カバリングインデックスは 「インデックスでクエリが完結している」 という前提で成立しているため、SELECT や WHERE に 1 列追加しただけで簡単に崩れます。

-- カバリング成立: subsidiary_id と eur_value のみ
SELECT SUM(eur_value) FROM sales WHERE subsidiary_id = ?;

-- WHERE に sale_date を追加 → sale_date はインデックスに無いのでテーブルアクセスが発生
SELECT SUM(eur_value) FROM sales WHERE subsidiary_id = ? AND sale_date > ?;

sale_date で絞り込んだ方が選択行数が減るのに、応答時間がかえって悪化する」という 不合理な変化 が起きるのが特徴です。サブパーセントの絞り込みでも、カバリングが崩れることでテーブルアクセスが復活して何倍も遅くなるケースがあります。

クエリを変更する前には必ず実行計画を確認しましょう。

INCLUDE 句(非キー列)

SQL Server / PostgreSQL 11+ / Db2 では、インデックスのキー列とは別に 非キー列 をリーフノードに保存できる構文があります。

-- SQL Server / PostgreSQL 11+ / Db2
CREATE INDEX empsubupnam
    ON employees (subsidiary_id, last_name)
    INCLUDE (phone_number, first_name);

INCLUDE で指定した列は アクセス述語としては使えません(インデックスツリーの並び順には参加しない)が、リーフに含まれているのでカバリング成立に貢献します。

「カバリングのためだけに列を足したい」「ツリーの分岐は変えずにペイロードだけ増やしたい」という用途に向いています。MySQL は対応する構文がないため、INCLUDE 相当を実現したい場合はキー列として追加するしかありません。

過剰に作らない

「インデックスのみのスキャンを狙って欲張って列を増やす」のは典型的なアンチパターンです。WHERE 句に含まれない列をインデックスに混ぜると:

  • インデックスサイズが膨らむ
  • INSERT/UPDATE のコストが上がる(書き込みコストの解説 も参照)
  • ストレージ消費が増える

設計の優先順位は以下です。

  1. WHERE 句などアクセス述語を最優先 に列順を決める
  2. SELECT 句を考えずにインデックスを作る
  3. 必要があれば後からカバリング目的で列を追加(または INCLUDE

「最初からカバリングを狙って全 SELECT 列を入れる」のは過剰設計のリスクが高いため、まずは普通の B-tree インデックスで設計し、計測して効果が見込めるクエリに対してのみカバリング化を検討するのが定石です。

まとめ

  • カバリングインデックスは SELECT/WHERE で参照される全列をインデックスに含めることで、テーブル本体へのアクセスを完全に省略する技法
  • 効果は大きく「最強のチューニング法のひとつ」と評価されるが、WHERE 列を 1 つ足すだけで簡単に崩れる
  • INCLUDE 句(SQL Server / PostgreSQL 11+ / Db2)は非キー列をリーフに保存できるカバリング専用の機能
  • 「最初からカバリング狙い」は過剰設計のリスク。WHERE を優先し、必要があれば後から拡張する

参考

タグ: MySQL , PostgreSQL , パフォーマンスチューニング