この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:5
ページ更新者:T
更新日時:2026-06-11 07:12:00

タイトル: MySQLで実行計画を表示する方法
SEOタイトル: 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 は実行するので注意)。