19.

MySQL 実行計画 (EXPLAIN) の完全ガイド(FORMAT=JSON / ANALYZE / Visual Explain)

編集
この記事の要点
  • EXPLAIN SELECT ... で実行計画を表示
  • MySQL 8.0+ の EXPLAIN ANALYZE は実際に実行して実時間・行数を計測
  • EXPLAIN FORMAT=JSON でコスト・bloated rows まで詳細表示
  • 主要フィールド: type (ALL/index/ref/eq_ref/const)、keyrowsExtra
  • type = ALL は全件スキャン警告。const / eq_ref が最速
  • MySQL Workbench の Visual Explain でツリー図表示
  • PostgreSQL なら EXPLAIN (ANALYZE, BUFFERS) でバッファヒット率も

EXPLAIN の基本

SQL の実行計画 (Execution Plan) は、オプティマイザが「どの索引を使い、どの順でテーブルにアクセスするか」を表したもの。性能チューニングには必須の情報です。

-- 基本
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';

-- 出力例
+----+-------------+-------+------+-------------------+----------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys     | key      | key_len | ref  | rows | Extra                    |
+----+-------------+-------+------+-------------------+----------+---------+------+------+--------------------------+
|  1 | SIMPLE      | users | ref  | idx_users_email   | idx_em.. | 768     | const|    1 | Using where; Using index |
+----+-------------+-------+------+-------------------+----------+---------+------+------+--------------------------+

主要フィールドの意味

意味注目ポイント
idクエリ内の SELECT 通し番号サブクエリ / UNION で複数になる
select_typeSIMPLE / PRIMARY / SUBQUERY / DERIVED / UNION ...派生テーブルが多いと遅い兆候
table対象テーブル名(alias)JOIN 順を読む手がかり
typeアクセス種別(★ 最重要)ALL は黄信号、const/eq_ref が良
possible_keys利用候補のインデックス空ならインデックス無し
key実際に使われたインデックスNULL なら全件スキャン
key_len使われたインデックスの長さ複合 index でどこまで効くか
rows読むと推定される行数大きいと再考の余地
filteredWHERE 通過率の推定 %低いと WHERE で大量フィルタリング
Extra追加情報(Using where 等)「Using filesort」「Using temporary」は要注意

type の階層(速い順)

type説明速度
systemテーブル 1 行のみ★★★★★
const主キー / UNIQUE 一致で 1 行★★★★★
eq_refJOIN 相手から主キー / UNIQUE で 1 行ずつ★★★★
ref非ユニーク index で複数行★★★
rangeindex 範囲スキャン (BETWEEN 等)★★★
indexindex 全スキャン(テーブル全件より速い)★★
ALLテーブル全件スキャン(フルテーブルスキャン)

Extra でよく出る警告

Extra意味対処
Using whereWHERE で行をフィルタリング通常は問題なし
Using indexCovering Index(テーブル本体を読まずに済む)★ 良い兆候
Using filesortORDER BY のために追加のソートORDER BY 列にも index、または複合 index 順序を見直し
Using temporary中間結果のために一時テーブル作成GROUP BY / DISTINCT 見直し
Using join bufferJOIN に index 無しJOIN 条件カラムに index 追加
Range checked for each recordJOIN 中に range アクセスを毎行判定JOIN 順や index を見直し

EXPLAIN FORMAT=JSON

各演算子のcost / 読み取り bytes / 行推定まで詳細表示します。チューニングには必須。

EXPLAIN FORMAT=JSON
SELECT u.id, u.name, o.total
FROM   users u
JOIN   orders o ON u.id = o.user_id
WHERE  u.email = 'alice@example.com';

-- 出力(抜粋)
{
  "query_block": {
    "select_id": 1,
    "cost_info": { "query_cost": "2.85" },
    "nested_loop": [
      {
        "table": {
          "table_name": "u",
          "access_type": "ref",
          "key": "idx_users_email",
          "rows_examined_per_scan": 1,
          "rows_produced_per_join": 1,
          "filtered": "100.00",
          "cost_info": { "read_cost": "0.25", "eval_cost": "0.10" }
        }
      },
      { "table": { "table_name": "o", "access_type": "ref", ... } }
    ]
  }
}

EXPLAIN ANALYZE (MySQL 8.0.18+)

実際にクエリを実行して、推定値ではなく実測の時間と行数を返します。推定と実測の乖離を発見するのに有用。

EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;

-- 出力例
-> Limit: 10 row(s)  (cost=4.20 rows=10) (actual time=0.124..0.142 rows=10 loops=1)
    -> Sort: orders.created_at DESC, limit input to 10 rows per chunk
        -> Index lookup on orders using idx_user_id (user_id=100)
           (cost=4.20 rows=42) (actual time=0.050..0.110 rows=42 loops=1)

-- ★ cost=4.20 rows=42 が推定
-- ★ actual time=0.050..0.110 rows=42 が実測
-- 推定と実測が大きく違う場合 → 統計が古い (ANALYZE TABLE 必要)

Visual Explain (MySQL Workbench)

MySQL Workbench を使うと、実行計画をカラフルなツリー図で見られます。

  1. クエリを書く
  2. 稲妻アイコン横の下三角 → 「Visual Explain」を選択
  3. 各ノードの色: 緑(高速)→ 黄 → 赤(遅い)
  4. 赤いノードにマウスオーバー → 詳細とアドバイス表示

SHOW WARNINGS でリライト後のクエリを見る

EXPLAIN SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
SHOW WARNINGS;
-- → オプティマイザが書き換えた実際のクエリが見える
-- → サブクエリが JOIN に変換されていることが多い

パフォーマンススキーマで統計を取る

-- 全クエリの平均実行時間 TOP 10
SELECT digest_text, count_star, avg_timer_wait/1e9 AS avg_ms
FROM   performance_schema.events_statements_summary_by_digest
ORDER BY avg_timer_wait DESC
LIMIT 10;

-- 特定テーブルへのアクセス統計
SELECT object_schema, object_name, count_star, sum_timer_wait/1e9 AS sum_ms
FROM   performance_schema.table_io_waits_summary_by_table
WHERE  object_schema = DATABASE()
ORDER BY sum_timer_wait DESC;

PostgreSQL の EXPLAIN との比較

-- PostgreSQL
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT ...;

-- BUFFERS の例
-> Index Scan using idx_users_email on users  (cost=0.42..8.44 rows=1 width=64)
       (actual time=0.025..0.027 rows=1 loops=1)
   Index Cond: (email = 'alice@example.com')
   Buffers: shared hit=4              ← バッファヒット 4 ブロック
   I/O Timings: read=0.000 write=0.000

-- pgAdmin / DataGrip でグラフィカルな実行計画も見られる

典型的な改善パターン

症状原因対処
type=ALLWHERE 列に index 無しCREATE INDEX
Using filesortORDER BY が index と不一致複合 index で並び順吸収
Using temporaryGROUP BY / DISTINCT が index 外GROUP BY 列にも index
rows 推定が実測と大きく違う統計古いANALYZE TABLE
JOIN で type=ALLJOIN 条件に index 無しFK 列に index

FAQ

Q: 本番の遅いクエリを後追いで EXPLAIN したい
A: スロークエリログ + mysqldumpslow または pt-query-digest で抽出 → EXPLAIN し直し。Performance Schema からも統計取得可。

Q: EXPLAIN と実際の実行で計画が変わる?
A: 統計情報が古い、サーバ負荷、バインド変数の値などで変わることがあります。本番環境で EXPLAIN ANALYZE を直接打つのが確実(読み取りクエリのみ)。

Q: UPDATEDELETE も EXPLAIN できる?
A: MySQL 5.6+ で可能。EXPLAIN UPDATE / DELETE / INSERT で実行計画が出ます。実際には更新されません(EXPLAIN ANALYZE は実行するので注意)。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. ダウンロード&インストール方法(Windows)
  2. インストール方法(Linux)
  3. コマンド一覧
  4. SQL
  5. データ型
  6. 関数
  7. 管理ツール
  8. 設定
  9. パフォーマンスチューニング関連
  10. エクスポートおよびインポート
  11. エラー&トラブル
  12. 文字コードの確認
  13. 実行中の SQL の状態確認およびプロセスキルの方法
  14. パスワードの無効化設定
  15. root ユーザーの初期パスワード確認方法
  16. rootユーザーのパスワード変更方法
  17. LIMIT, OFFSET の始まりと挙動
  18. mysqlのバージョン確認方法
  19. MySQLで実行計画を表示する方法
  20. レプリケーションのステータス確認方法
  21. 中央値の導き方(バージョン8未満)
  22. 階層SQL(バージョン8未満)
  23. パーセンタイルの導き方
  24. 特定スキーマの全テーブルの全カラム情報を取得する方法

最近更新/作成されたページ