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

ORDER BY / GROUP BY をインデックスで速くする

この記事は、基礎から複雑なSQL,SQLチューニングまでSQLの実践的なスキルを1からマスターする「SQL入門講座の一部」です。
ORDER BY や GROUP BY、DISTINCT で発生する並べ替え作業を、対象列にインデックスを作って省略する書き方を確かめます。

並び替えの一時ソートをインデックスで消す

ORDER BYで並び替えるとき、対象列にインデックスが無いと、いったん全行を取り出してから並べ替える作業が必要になります。

この一時的な並べ替えを実行計画ではUSE TEMP B-TREE FOR ORDER BY(並べ替えのための一時的な作業用構造を作る)と表示します。

ここで、並び替えたい列にインデックスを作ると話が変わります。

インデックスは作った時点でその列の順に並んでいます。

インデックスの並びを使えばすぐに結果が得られます。

一時ソートが不要になり、計画からUSE TEMP B-TREEが消えます。

一時ソートは全 5 万行をいったん作業用構造に並べ直す処理で、行数に比例して重くなります。

LIMIT 10で最終的に 10 行しか返さなくても、上位 10 行を決めるには全行を並べ替える必要があるため負担は変わりません。

この記事では、「ソートをインデックスで消す」のを試して見ましょう。

インデックスなしの ORDER BY とインデックスありの違い
ORDER BY sale_dateインデックスなし全行を取り出すその場で並べ替え(USE TEMP B-TREE)結果sale_date にインデックスあり並びをたどるだけ一時ソート不要結果(TEMP B-TREE なし)
インデックスが無い ORDER BY は全行を取り出してから一時ソートします(USE TEMP B-TREE)。並び替え列にインデックスがあると、その並びをたどるだけで済み一時ソートが不要になります。
-- amount で並べた上位 10 件を取り出す例
-- インデックスがあれば並びをたどるだけで済む
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(amount);

EXPLAIN QUERY PLAN
SELECT sale_id, amount
FROM perf_sales
ORDER BY amount
LIMIT 10;

「売上を日付順に並べて先頭の数件を見たい」という要件を考えます。sale_dateで並べ替えるクエリの実行計画を、インデックスなし・ありの 2 通りで見比べ、USE TEMP B-TREE FOR ORDER BYが出るかどうかを観察します。インデックスはこのコンソール 1 回の実行で作り直し、計画を表示するところまでを自己完結させます。(正しく実行できれば解説が表示されます)

① まずDROP INDEX IF EXISTSsale_date用のインデックスを消し、sale_dateで並べた先頭 10 件の実行計画をEXPLAIN QUERY PLANで表示してください(インデックスなしの状態)。

② 続けて、sale_dateの単一列インデックスを作成してください。

③ 同じ並び替えクエリの実行計画をもう一度表示し、① で出ていた一時ソートの表示が消えるか見比べてください。

SQL エディタ

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

GROUP BY もインデックスの並びでまとめられる

GROUP BYも、グループ化のために値が同じ行を隣り合わせる必要があります。

インデックスが無いと一時ソートでUSE TEMP B-TREE FOR GROUP BYが発生します。

グループ化する列にインデックスを作ると、同じ値の行がインデックスの並びの中ですでに連続しているため、その並びを順にたどるだけでグループを切り出せます。

たとえばGROUP BY emp_idならemp_idにインデックスを作ると、emp_idごとのまとまりが並びの中で連続しているため、一時ソートなしで集計できます。

集計対象の列も一緒にインデックスへ含めれば、テーブル本体へ戻らずに済む形(前記事のインデックスオンリースキャン)と組み合わせられます。

GROUP BY をインデックスの並びでまとめる
GROUP BY emp_idインデックスなし全行を取り出す一時ソートでグループ化(USE TEMP B-TREE)結果emp_id にインデックスあり同じ値が連続たどるだけ結果(TEMP B-TREE なし)
インデックスが無い GROUP BY は全行を取り出してから一時ソートでグループ化します(USE TEMP B-TREE)。グループ化列にインデックスがあると同じ値の行が並びの中で連続しているため、たどるだけでグループを切り出せます。
-- region ごとの件数を集計する例
-- region のインデックスがあれば GROUP BY の一時ソートが不要
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_idでグループ化するクエリの実行計画を、インデックスなし・ありの 2 通りで見比べ、USE TEMP B-TREE FOR GROUP BYが出るかどうかを観察します。

① まずDROP INDEX IF EXISTSでインデックスを消し、emp_idごとの件数を集計するクエリの実行計画をEXPLAIN QUERY PLANで表示してください(インデックスなしの状態)。

② 続けて、emp_idの単一列インデックスを作成してください。

③ 同じ集計クエリの実行計画をもう一度表示し、① で出ていた一時ソートの表示が消えるか見比べてください。

SQL エディタ

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

DISTINCT も同じ仕組みで一時ソートを消せる

DISTINCT(重複を取り除いて一意な値だけを返す指定)も、重複判定のために値を並べる必要があり、インデックスが無いと一時ソートでUSE TEMP B-TREE FOR DISTINCTが発生します。

仕組みはGROUP BYと同じで、対象の列にインデックスがあると同じ値が並びの中で連続しているため、隣どうしを比べるだけで重複を取り除けます。

SELECT DISTINCT product FROM perf_salesのように 1 列の一意な値を取り出すなら、productにインデックスを作ると一時ソートが不要になります。

並び替え・グループ化・重複排除は、いずれも「値を並べる必要がある処理」という点で共通しており、対象列のインデックスで一時ソートを避けられます。

ORDER BY / GROUP BY / DISTINCT は同じ仕組みで救える
値を並べる必要がある処理対象列のインデックスで回避ORDER BYTEMP B-TREEFOR ORDER BY を回避GROUP BYTEMP B-TREEFOR GROUP BY を回避DISTINCTTEMP B-TREEFOR DISTINCT を回避
並び替え・グループ化・重複排除はいずれも値を並べる必要がある処理です。対象列にインデックスがあれば並びをたどるだけで済み、共通して USE TEMP B-TREE を回避できます。
-- region の一意な値を取り出す例
-- region のインデックスで DISTINCT の一時ソートを回避
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(region);

EXPLAIN QUERY PLAN
SELECT DISTINCT region
FROM perf_sales;

「取り扱った商品コードの一覧(重複なし)を出したい」という要件を考えます。productの一意な値を取り出すクエリの実行計画を、インデックスなし・ありの 2 通りで見比べ、USE TEMP B-TREE FOR DISTINCTが出るかどうかを観察します。

① まずDROP INDEX IF EXISTSでインデックスを消し、productの一意な値を取り出すクエリの実行計画をEXPLAIN QUERY PLANで表示してください(インデックスなしの状態)。

② 続けて、productの単一列インデックスを作成してください。

③ 同じ重複排除クエリの実行計画をもう一度表示し、① で出ていた一時ソートの表示が消えるか見比べてください。

SQL エディタ

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

理解度チェック

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

Q1インデックスのない列でORDER BYをしたとき、実行計画に現れるのはどれですか。

Q2GROUP BY emp_idの一時ソートをインデックスで回避できるのはなぜですか。

Q3ORDER BY / GROUP BY / DISTINCT の一時ソートをインデックスで回避できる共通の理由はどれですか。