Q1実行計画にSCAN perf_salesと出ているとき、その意味として正しいものはどれですか。
実行計画の読み方 — EXPLAIN QUERY PLAN
この記事は、基礎から複雑なSQL,SQLチューニングまでSQLの実践的なスキルを1からマスターする「SQL入門講座の一部」です。
EXPLAIN QUERY PLAN に出る実行計画の用語を 1 つずつ整理。フルテーブルスキャンとインデックス検索の違い、並べ替え用の一時領域、インデックスオンリースキャン、相関サブクエリ、マテリアライズを、索引の有無で計画がどう変わるかを実際に動かして読み解きます。
本記事で使うデータ — perf_sales(5 万行の売上)
クエリが速いか遅いかを判断するとき、最初に見るのが実行計画(クエリプラン)です。
データベースは SQL を受け取ると「どのテーブルをどの順に、どうやって読むか」という手順を内部で決めます。
その手順を文字で見せてくれるのがEXPLAIN QUERY PLANです。
クエリの前にこの語を付けて実行すると、結果の代わりに実行手順が表示されます。
SCAN と SEARCH — フルスキャンとインデックススキャン
実行計画でまず覚えるのがSCANとSEARCHの 2 語です。
SCAN perf_salesはテーブルの先頭から末尾まで全行を順に読む(フルスキャン)という意味です。
索引が無い列で絞り込むと、データベースは「どの行が該当するか分からない」ため、5 万行すべてを 1 行ずつ確認します。
一方SEARCH perf_sales USING INDEX 索引名 (列=?)は、索引をたどって該当行だけにピンポイントで到達する(インデックススキャン)という意味です。
索引(CREATE INDEX)を作ると、その列で絞る条件は全行を見ずに済むようになり、実行計画がSCANからSEARCH ... USING INDEXに変わります。
-- region で絞ると、索引が無いので全行を読む
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM perf_sales WHERE region = 'East';
--> SCAN perf_sales
-- region に索引を作ってから同じ計画を見ると
DROP INDEX IF EXISTS ix_region;
CREATE INDEX ix_region ON perf_sales(region);
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM perf_sales WHERE region = 'East';
--> SEARCH perf_sales USING INDEX ix_region (region=?)
USE TEMP B-TREE — 並べ替え用の一時領域
USE TEMP B-TREE FOR ORDER BY(一時 B ツリー)は、ORDER BYの対象列に索引が無いため、結果を並べ替えるための一時的な作業領域を作っている、という意味です。
5 万行を一時領域にいったん入れて並べ直すため、行数が多いほど重くなります。
並べ替えに使える索引があると、この行は計画から消え、索引の並び順をそのまま使えるようになります。
-- amount 昇順 上位 10 件 → 索引が無いので並べ替え用の一時領域が必要
EXPLAIN QUERY PLAN
SELECT sale_id, amount FROM perf_sales ORDER BY amount LIMIT 10;
--> SCAN perf_sales
--> USE TEMP B-TREE FOR ORDER BY
-- amount に索引を作ると並べ替えに索引の順序をそのまま使える
DROP INDEX IF EXISTS ix_amount;
CREATE INDEX ix_amount ON perf_sales(amount);
EXPLAIN QUERY PLAN
SELECT sale_id, amount FROM perf_sales ORDER BY amount LIMIT 10;
--> SEARCH perf_sales USING INDEX ix_amount
-- USE TEMP B-TREE は計画から消える
USING COVERING INDEX — テーブル本体を読まない索引
USING COVERING INDEX(インデックスオンリースキャン、英語ではカバリングインデックスとも)は、クエリが必要とする列がすべて索引の中に含まれていて、テーブル本体を読まずに索引だけで結果を作れたという意味です。
索引はテーブルから必要な列だけを抜き出した小さなコピーなので、本体を読まない分だけ速くなります。
SEARCH ... USING INDEXよりさらに進んだ状態で、計画にUSING COVERING INDEXが出たら最も効率の良い読み方ができています。
-- emp_id だけの索引 → 索引で行を見つけて、続いて本体から amount を読む
DROP INDEX IF EXISTS ix_emp;
CREATE INDEX ix_emp ON perf_sales(emp_id);
EXPLAIN QUERY PLAN
SELECT emp_id, amount FROM perf_sales WHERE emp_id = 7;
--> SEARCH perf_sales USING INDEX ix_emp (emp_id=?)
-- (本体も読む)
-- (emp_id, amount) の複合索引 → 必要列が全部索引内なので本体を読まない
DROP INDEX IF EXISTS ix_emp_amount;
CREATE INDEX ix_emp_amount ON perf_sales(emp_id, amount);
EXPLAIN QUERY PLAN
SELECT emp_id, amount FROM perf_sales WHERE emp_id = 7;
--> SEARCH perf_sales USING COVERING INDEX ix_emp_amount (emp_id=?)
サブクエリの計画 — CORRELATED と MATERIALIZE
サブクエリを含むクエリの計画には専用の語が出ます。
CORRELATED SCALAR SUBQUERY(相関スカラサブクエリ)は、外側の行ごとに値が変わるサブクエリを各行で実行し直しているという意味です。
外側が 5 万行ならサブクエリも 5 万回走る可能性があり、計画にこの語が出たら書き換えの検討対象になります。
一方MATERIALIZE(マテリアライズ、実体化)は、サブクエリや CTE の結果を一度だけ計算して一時表に保存し、それを使い回すという意味です。
同じ中間結果を何度も参照するときに、計算を 1 回で済ませる最適化です。
計画にこの 2 語のどちらが出るかで、サブクエリが「毎回計算されている」のか「1 回で済んでいる」のかが読み取れます。
-- 相関スカラサブクエリ: 外側の行ごとに「その region の最大額」を引き直す
EXPLAIN QUERY PLAN
SELECT s.sale_id, s.amount
FROM perf_sales s
WHERE s.amount = (
SELECT MAX(amount) FROM perf_sales x WHERE x.region = s.region
)
LIMIT 10;
--> CORRELATED SCALAR SUBQUERY という行が出る
-- 派生表(FROM 句のサブクエリ)は一度だけ計算される
EXPLAIN QUERY PLAN
SELECT t.region, t.s FROM (
SELECT region, SUM(amount) AS s FROM perf_sales GROUP BY region
) t
WHERE t.s > 100000000;
--> MATERIALIZE という行が出ることがある
他の RDBMS との対応 — MySQL / PostgreSQL / Oracle
本記事で見た計画用語は SQLite のEXPLAIN QUERY PLANのものですが、同じ概念は MySQL / PostgreSQL / Oracle でも別の名前で表示されます。
まず各 DB で実行計画を出すコマンドを確認したうえで、計画に出てくる用語の早見表を見比べると、別 DB に移ったときにそのまま読み解けます。
各 DB での実行計画の表示方法
- SQLite: クエリの前に
EXPLAIN QUERY PLANを付ける(例:EXPLAIN QUERY PLAN SELECT ...;)。実行はせず計画だけを表示する。EXPLAIN単体だと低水準のバイトコードが出るので、人が読むのはEXPLAIN QUERY PLANの方 - MySQL: クエリの前に
EXPLAINを付ける(例:EXPLAIN SELECT ...;)。MySQL 8.0+ はEXPLAIN ANALYZE SELECT ...;で実測時間と実際の行数も得られる。JSON 形式が欲しいときはEXPLAIN FORMAT=JSON SELECT ...; - PostgreSQL: クエリの前に
EXPLAINを付ける(例:EXPLAIN SELECT ...;)。これは推定プランのみ。実測も見るならEXPLAIN ANALYZE SELECT ...;、I/O バッファまで詳しく見るならEXPLAIN (ANALYZE, BUFFERS) SELECT ...; - Oracle:
EXPLAIN PLAN FOR <クエリ>;で計画を内部表に保存し、続いてSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);で表示する 2 段構え。簡単に見たいときはSET AUTOTRACE ONセッションオプションも使える
計画に出てくる用語の対応表
| SQLite (EXPLAIN QUERY PLAN) | MySQL (EXPLAIN) | PostgreSQL (EXPLAIN) | Oracle (EXPLAIN PLAN) |
|---|---|---|---|
SCAN(フルテーブルスキャン) | type=ALL | Seq Scan | TABLE ACCESS FULL |
SEARCH ... USING INDEX | type=ref / range / eq_ref | Index Scan / Bitmap Index Scan | INDEX RANGE SCAN / INDEX UNIQUE SCAN |
USING COVERING INDEX | Extra: Using index | Index Only Scan | INDEX FAST FULL SCAN |
USE TEMP B-TREE FOR ORDER BY | Extra: Using filesort | Sort | SORT ORDER BY |
USE TEMP B-TREE FOR GROUP BY | Extra: Using temporary; Using filesort | HashAggregate / GroupAggregate | HASH GROUP BY / SORT GROUP BY |
CORRELATED SCALAR SUBQUERY | DEPENDENT SUBQUERY | SubPlan(相関) | 相関サブクエリ(FILTER 内) |
MATERIALIZE | Extra: Using temporary | Materialize / CTE Scan | 一時表変換(TEMP TABLE TRANSFORMATION) |
用語は違っても、データベースが内部でしていることはほぼ同じです。「全行を読んでいるのか、索引で絞っているのか、並べ替えに作業領域が要るのか、サブクエリを何度も走らせていないか」を、各 DB のEXPLAIN出力から読み取れるようになるのが本記事の到達点です。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2実行計画のUSING COVERING INDEXがUSING INDEXよりさらに効率的なのはなぜですか。
Q3実行計画にCORRELATED SCALAR SUBQUERYが出ているとき、最も適切な読み取りはどれですか。