#02 データベースの高速化

EXPLAINで実行計画を読む

スロークエリを発見したら、次は「なぜ遅いのか」を調べます。そのための最強ツールが EXPLAIN? MySQLがSQLをどのように実行するかの「実行計画」を表示するコマンド。インデックスが使われているか、何行スキャンしているかなどを確認でき、チューニングの出発点になる。 です。EXPLAIN はクエリの前に付けるだけで、MySQL がそのクエリをどのように実行しようとしているか——どのインデックスを使うか、何行スキャンするか——を表示してくれます。

EXPLAINの各列の説明 列名 意味 チェックポイント type インデックスの使われ方 ALL は要改善 key 実際に使われたインデックス名 NULL なら未使用 rows スキャン予測行数(統計情報から推定) 大きいほど要注意 Extra 実行方法の補足情報 Using filesort / temporary に注意 key_len 使われたインデックスのバイト数 複合インデックスの利用度確認 select_type クエリの種類(SIMPLE / SUBQUERY 等) DERIVED は最適化の余地あり id / table ステップ番号 / 対象テーブル名 複数行はJOINや派生テーブル 最重要チェック項目 確認推奨 EXPLAIN SELECT ... の結果を読む習慣をつけよう
図1: EXPLAINの各列の説明

EXPLAIN の基本的な使い方

EXPLAIN SELECT * FROM orders WHERE user_id = 100;

これだけです。SELECT の前に EXPLAIN を付けるだけで実行計画が得られます。

各列の意味

id

クエリのステップ番号です。単純なクエリは1つだけですが、サブクエリや UNION があると複数の行が現れます。同じ id を持つ行は同じステップで実行されます。

select_type

クエリの種類です。

意味
SIMPLEサブクエリも UNION もない単純な SELECT
PRIMARY外側のクエリ
SUBQUERYサブクエリ
DERIVEDFROM 句のサブクエリ(derived table)

type(最重要)

インデックスの使われ方を表す最も重要な列です。

key と key_len

key は実際に使われたインデックス名、key_len はインデックスのうち何バイト使ったかです。複合インデックスでどのカラムまで使われたかを key_len から推測できます。

rows

MySQL が返すと推定している行数です。実際の行数ではなくあくまで統計情報からの推定ですが、100行と100万行では意味がまったく異なります。

Extra

クエリの実行方法に関する追加情報です。

意味
Using indexインデックスだけで完結(テーブルアクセス不要)
Using whereWHERE による絞り込みが発生
Using filesortORDER BY のソートにインデックスを使えなかった
Using temporary一時テーブルが作成された(GROUP BY など)

Using filesortUsing temporary はパフォーマンス上の警告サインです。

type の読み方

type の良し悪しランキング(左が最良) 最良 最悪 const PK/UNIQUE等値 1行確定 eq_ref JOIN時のUNIQUE 最高効率 ref 非UNIQUE等値 良好 range 範囲検索 許容範囲 index インデックス全件 要注意 ALL フルテーブルスキャン 要改善! 実例 -- const WHERE id = 42 PK等値 → 1行確定 -- ref WHERE user_id = 100 非ユニーク → 複数行ヒット -- ALL (要改善) WHERE status = 'pending' インデックスなし → 全行スキャン Extra 列の重要な値 Using index → カバリングインデックス(最高状態) Using filesort → ORDER BY にインデックスを使えなかった(要注意) Using temporary → 一時テーブルが作成された(GROUP BY 等、要注意)
図2: typeの良し悪しランキング

type 列は左が良く、右に行くほど悪化します。

const > eq_ref > ref > range > index > ALL

const / eq_ref(最良)

プライマリキーまたはユニークインデックスへの等値検索。1行だけが確定します。

-- type: const(プライマリキーで1行確定)
EXPLAIN SELECT * FROM users WHERE id = 42;

ref

ユニークでないインデックスへの等値検索。複数行がヒットする可能性があります。

-- type: ref(user_idにインデックスがある場合)
EXPLAIN SELECT * FROM orders WHERE user_id = 100;

range

インデックスを使った範囲検索。BETWEEN><IN などが該当します。

-- type: range
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-01-01';

index

インデックス全体をスキャン。テーブル全スキャンよりはましですが、大量行では遅くなります。

ALL(最悪)

フルテーブルスキャン? インデックスを使わずにテーブルの全行を順番に読み込む操作。少量データなら問題ないが、大きなテーブルでは深刻なパフォーマンス問題になる。EXPLAINのtypeが`ALL`の場合がこれ。 ——テーブルのすべての行を読む最も非効率な方法です。行数が多いテーブルで type: ALL が出たら要注意です。

-- インデックスなし、type: ALL
EXPLAIN SELECT * FROM orders WHERE status = 'pending';

EXPLAIN ANALYZE(MySQL 8.0以降)

MySQL 8.0.18から使える EXPLAIN ANALYZE は、推定値だけでなく実際の実行統計も表示します。

EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;

出力例:

-> Index lookup on orders using idx_user_id (user_id=100)
   (cost=4.51 rows=12) (actual time=0.082..0.124 rows=15 loops=1)

rows=12(推定)と rows=15(実際)の乖離が大きいほど、統計情報が古くなっているサインです。そのときは ANALYZE TABLE orders; で統計を更新しましょう。

実例:スロークエリを EXPLAIN で分析する

前回のエピソードで発見したスロークエリを分析してみます。

-- 問題のクエリ
SELECT * FROM orders WHERE status = 'pending';
EXPLAIN SELECT * FROM orders WHERE status = 'pending'\G
* 1. row *
           id: 1
  select_type: SIMPLE
        table: orders
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 845231
        Extra: Using where

type: ALLkey: NULLrows: 845231——典型的なフルスキャンです。status カラムにインデックスがないため、84万行すべてを読んでいます。

改善策としてインデックスを追加します。

ALTER TABLE orders ADD INDEX idx_status (status);

再度 EXPLAIN を実行すると:

         type: ref
possible_keys: idx_status
          key: idx_status
      key_len: 1023
         rows: 2341
        Extra: Using index condition

rows が84万から2341に激減しました。実行時間も3.5秒から数ミリ秒に短縮されます。

EXPLAIN を読む習慣をつけることで、インデックスの効果を事前に確認してからデプロイできます。次のエピソードでは、そのインデックスの仕組み—— B-treeインデックス? MySQLのデフォルトのインデックス構造。バランス木(Balanced Tree)で値を管理し、等値検索・範囲検索・ORDER BY の高速化に有効。ほとんどのユースケースでこれを使う。 ——を詳しく見ていきます。