こんにちは。 エーティーエルシステムズ 萩原です。
今回はSQL高速化テクニックの一つ「カバーリングインデックス」について投稿します。
この記事が皆さんの問題解決のきっかけになったら大変うれしく思います。
はじめに
データベースのパフォーマンスチューニングにおいて、インデックスの追加は基本中の基本です。しかし、「検索条件の列にインデックスを貼ったのに、思ったほど速くならない」という経験はありませんか?
SQL Serverにおいて、その原因の多くは「Key Lookup(キールックアップ)」という隠れたコストにあります。
今回は、このKey Lookupをなくし、クエリを劇的に高速化する「カバーリングインデックス」と、SQL Serverならではの「INCLUDE句」の活用方法について解説します。
カバーリングインデックスとは?
カバーリングインデックスとは、「SELECTクエリで要求されるすべての列が、インデックス内に含まれている状態」のことを指します。
SQL Serverで非クラスター化インデックス(Non-Clustered Index)を使って検索をする際、通常は以下の2ステップを踏みます。
- Index Seek: 非クラスター化インデックスを検索し、対象レコードの「クラスター化インデックスのキー(通常は主キー)」を見つける
- Key Lookup: 見つけたキーを使って、実際のテーブル(クラスター化インデックス)へアクセスし、SELECT句で指定された残りの列データを取得する
この「2」のKey Lookupは、ディスクへのランダムアクセスを伴うため非常にコストが高い処理です。対象レコード数が増えると、インデックスを使わずにテーブル全体をスキャン(Table Scan / Clustered Index Scan)した方がマシ、とSQL Serverのオプティマイザが判断してしまうこともあります。
カバーリングインデックスが成立していると、インデックスの中に必要な列がすべて揃っているため、ステップ2のKey Lookupを完全にスキップし、インデックスを読むだけで結果を返すことができます。
具体例:INCLUDE句を使った最適化
具体的なテーブルとクエリを例に見てみましょう。
CREATE TABLE Users (
UserId INT PRIMARY KEY CLUSTERED,
UserName NVARCHAR(50),
DepartmentId INT,
CreatedAt DATETIME2
);
ある部署(例:DepartmentId = 1)に所属するユーザーの UserId と UserName を取得するクエリが頻繁に実行されるとします。
SELECT UserId, UserName FROM Users WHERE DepartmentId = 1;
❌ 通常のインデックス(Key Lookupが発生)
検索条件である DepartmentId にのみインデックスを貼ります。
CREATE NONCLUSTERED INDEX IX_Users_DepartmentId
ON Users(DepartmentId);
SQL Serverは DepartmentId = 1 を素早く見つけますが、UserName の情報がインデックス内にないため、結果を返すためにテーブルデータへKey Lookupを実行します。(※UserIdは主キーなので暗黙的に含まれています)
⭕ カバーリングインデックス(INCLUDE句を使用)
SQL Serverでは、カバーリングインデックスを作成する際に、検索条件ではない列を INCLUDE 句 で付加するのがベストプラクティスです。
CREATE NONCLUSTERED INDEX IX_Users_DepartmentId_Inc
ON Users(DepartmentId) INCLUDE (UserName);
このインデックスが使われると、SQL Serverはインデックスのリーフノード(末端)に保存された UserName をそのまま読み取ることができます。重いKey Lookupがゼロになるため、パフォーマンスが劇的に向上します。
💡 なぜキー列にすべて含めないのか?
ON Users(DepartmentId, UserName) のように複合インデックスにしてもカバーリングインデックスにはなります。しかし、INCLUDE 句を使うことで以下のメリットがあります。
- インデックスのツリー構造(中間層)には
UserNameが含まれないため、インデックスのサイズ肥大化を抑えられる - インデックスキーの最大サイズ制限(900バイトまたは1700バイト)を回避できる
-
UserNameの更新時にインデックスツリーの並び替え(ソート)が発生しない
SSMS(実行計画)での確認方法
カバーリングインデックスが効いているかは、SQL Server Management Studio (SSMS) の「実際の実行計画を含める (Ctrl + M)」で確認できます。
-
チューニング前: 実行計画に
Key Lookupというアイコンが表示され、高いコスト(%)を占めている。 -
チューニング後:
Key Lookupが消滅し、Index Seek (NonClustered)だけになっている。
また、クエリ実行前に SET STATISTICS IO ON; を実行し「メッセージ」タブを確認してみてください。チューニング後は 「論理読み取り数 (logical reads)」 の数値が劇的に減っているはずです。
⚠️ 注意点(デメリット)
カバーリングインデックスは強力ですが、リスクもあります。
以下のトレードオフを意識してください。
-
更新コストの増加
INCLUDE句に列を追加すればするほど、INSERT,UPDATE,DELETE実行時にインデックス側のデータも更新するオーバーヘッドが増加します。 -
ストレージとメモリの圧迫
インデックスのサイズが大きくなります。SQL Serverのパフォーマンスは「データがメモリ上にどれだけ乗っているか」に依存するため、無駄な列をINCLUDEしすぎるとメモリを圧迫し、システム全体のパフォーマンス低下を招く恐れがあります。
「SELECT *」のようなクエリに対して、全ての列を含んだカバーリングインデックスを作ろうとするのは絶対にやめましょう。
まとめ
- SQL Serverが遅い原因の多くは Key Lookup にある。
- 必要な列をインデックスに含める カバーリングインデックス でこれを回避できる。
- 実装には、インデックスサイズを抑えつつデータを付加できる INCLUDE句 を使うのがベストプラクティス。
- 実行計画でKey Lookupが消え、Index Seekのみになっているか確認する。
どうしても重いクエリがある場合は、ピンポイントで INCLUDE 句を活用して、爆速なレスポンスを手に入れましょう!