順番に読み進めながら学べます

インデックスオンリースキャン — テーブル参照を省くインデックス

この記事は、基礎から複雑なSQL,SQLチューニングまでSQLの実践的なスキルを1からマスターする「SQL入門講座の一部」です。
クエリが触る列をすべてインデックスに含めると、テーブル本体への戻りが消える「インデックスオンリースキャン」(カバリングインデックス)を解説します。成立条件、1 列でも欠けると崩れる挙動、WHERE の絞り込み列を含めて 1 本にまとめる設計を、EXPLAIN QUERY PLAN で確かめます。

テーブルルックアップを省く — インデックスオンリースキャン

通常のインデックス探索は、インデックスで対象の行を見つけたあと、その行の他の列を読むためにテーブル本体に戻って 1 行ずつ読み直します。

この戻りをテーブルルックアップ(インデックスからテーブル本体の行を引きに行く処理)と呼びます。

返す行が多いとこの戻りが積み重なり、コストになります。

クエリが参照する列をすべて含むインデックスを使うと、必要な値がインデックスの中だけで揃うため、テーブル本体に戻る必要がなくなります。

インデックスだけで結果を返すこの動きをインデックスオンリースキャンと呼びます(このような全列を含むインデックスを、英語ではカバリングインデックスとも呼びます)。

通常の探索とインデックスオンリースキャンの違い
クエリが必要とする列の集合通常インデックス探索 (SEARCH)テーブル本体へ戻り他列を読み直す結果全列を含むインデックス→ インデックスオンリースキャンテーブル本体へ戻らない結果(USING COVERING INDEX)
通常のインデックス探索はインデックスで行を見つけたあとテーブル本体へ戻ります。必要な列が全部インデックスに入っていれば、テーブル本体へ戻らずインデックスだけで結果を返せます。
-- region だけを含むインデックスで region を集計する例
-- 参照列(region)が全部インデックスに入る → テーブル本体へ戻らない
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(region);

EXPLAIN QUERY PLAN
SELECT region, COUNT(*)
FROM perf_sales
GROUP BY region;

「担当者ごとの売上合計を出したい」という要件を考えます。このクエリが参照する列はemp_idamountの 2 つだけです。この 2 列を含むインデックスを作り、テーブル本体へ戻らずインデックスだけで結果が出る計画になることを確かめます。インデックスはこのコンソール 1 回の実行で作り直し、計画を表示するところまでを自己完結させます。(正しく実行できれば解説が表示されます)

DROP INDEX IF EXISTSでインデックスを消してください。

② クエリが参照するemp_idamountを両方含むインデックスを作成してください。

emp_idごとにamountの合計を出す集計クエリの実行計画をEXPLAIN QUERY PLANで表示し、テーブル本体を読まない計画になっているか確認してください。

SQL エディタ

クエリを実行してください

1 列でも欠けるとテーブル本体へ戻る

インデックスオンリースキャンが成立するのは、SELECT・WHERE・GROUP BY などクエリが触る列が 1 つ残らずインデックスに含まれているときだけです。

1 列でも足りないと、その列の値を読むためにテーブル本体へ戻る必要があり、計画からUSING COVERING INDEXが消えます。

たとえば(emp_id, amount)のインデックスに対してSELECT emp_id, SUM(amount), regionのようにregionを追加すると、regionはインデックスに無いためテーブル本体へ戻ります。

1 列欠けるとインデックスオンリースキャンが崩れる
INDEX (emp_id, amount)含む列: emp_id, amountSELECT emp_id,SUM(amount)全列が揃う→ インデックスのみで完結SELECT emp_id,SUM(amount), regionregion が無い→ 本体へ戻る
クエリが触る列が全部インデックスに含まれていればインデックスだけで完結します。1 列でもインデックスに無いと、その列を読むためテーブル本体へ戻り、インデックスオンリースキャンが崩れます。
-- (region, amount) のインデックスに対し、
-- 参照列に product を足すとインデックスオンリースキャンが崩れる例
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(region, amount);

-- region, SUM(amount) だけ → インデックスのみで完結
EXPLAIN QUERY PLAN
SELECT region, SUM(amount) FROM perf_sales GROUP BY region;

-- product を足す → インデックスに無いので本体へ戻る
EXPLAIN QUERY PLAN
SELECT region, SUM(amount), MAX(product) FROM perf_sales GROUP BY region;

同じインデックスに対し、参照する列がインデックスに収まる場合と、収まらない列を 1 つ足した場合とで実行計画がどう変わるかを観察します。emp_idamountの複合インデックスを作り、2 種類の集計クエリで計画を見比べてください。

DROP INDEX IF EXISTSでインデックスを消してください。

emp_idamountを含む複合インデックスを作成してください。

emp_idごとにamountの合計を出す集計クエリの実行計画を表示してください(インデックスに収まる)。

④ 続けて、同じ集計にregionの最大値も加えたクエリの実行計画を表示し、③ と表示がどう変わるか見比べてください(regionはインデックスに無い)。

SQL エディタ

クエリを実行してください

WHERE の絞り込み列も含めてインデックスオンリースキャンを保つ

インデックスに含めるべき列は SELECT に出てくるものだけではありません。

WHERE で絞り込む列もクエリが触る列なので、これもインデックスに含めて初めてテーブル本体に戻らずに済みます。

「絞り込み列 → 出力・集計列」の順で 1 本のインデックスにまとめると、絞り込みと値の取り出しが同じインデックス内で完結します。

たとえばWHERE region = 'East'で絞ってSUM(amount)を出すなら、(region, amount)のように絞り込み列regionを先頭、集計列amountを後ろにします。

regionで対象を絞り込みつつ、amountの値もインデックスから読めるため、本体への戻りが発生しません。

絞り込み列 → 出力列の順で 1 本にまとめる
WHERE region='East'SELECT SUM(amount)絞り込み: region取り出し: amountINDEX(region, amount)region で絞りamount を同時に読む本体へ戻らず完結絞り込み列を先頭に置くのが向く
WHERE の絞り込み列を先頭、SELECT・集計の列を後ろに 1 本のインデックスにまとめると、絞り込みと値の取り出しが同じインデックス内で完結しテーブル本体へ戻りません。
-- 絞り込み列(status) と集計列(amount) を 1 本にまとめる例
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(status, amount);

EXPLAIN QUERY PLAN
SELECT SUM(amount)
FROM perf_sales
WHERE status = 'pending';

「特定の地域に絞った売上合計を出したい」という要件を考えます。WHEREregionを絞り、amountの合計を出すクエリです。絞り込み列と集計列を 1 本のインデックスにまとめ、テーブル本体へ戻らない計画になることを確かめます。

DROP INDEX IF EXISTSでインデックスを消してください。

② 絞り込み列regionを先頭、集計列amountを後ろにした複合インデックスを作成してください。

regionで絞り込んでamountの合計を出す集計クエリの実行計画をEXPLAIN QUERY PLANで表示し、テーブル本体を読まない計画になっているか確認してください。

SQL エディタ

クエリを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1インデックスオンリースキャンでテーブル本体へ戻らずに済むのはなぜですか。

Q2(emp_id, amount)のインデックスがあるとき、インデックスオンリースキャンが崩れてテーブル本体へ戻るのはどのクエリですか。

Q3WHERE region = 'East'で絞りSUM(amount)を出すクエリでインデックスオンリースキャンにするインデックスとして適切なものはどれですか。