MySQL EXPLAIN ANALYZE を使った実行計画の実測検証
MySQL 8.0.18 以降では、EXPLAIN ANALYZE という新機能が利用可能になりました。これは従来の EXPLAIN が出力する理論上の実行計画に加え、実際にクエリを実行して実測値(実行時間・行数・ループ数など)を表示してくれる強力な分析ツールです。
この記事では、10万件のテストデータを使い、EXPLAIN ANALYZE の基本的な使い方と3種類のクエリによる実行計画の違いを実演形式で紹介します。
MySQLは8.4.5を利用しています。
テストデータの作成
検証には以下の2テーブルを使用します。
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    email VARCHAR(255)
);
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    amount DECIMAL(10,2),
    created_at DATETIME,
    INDEX idx_user_id (user_id)
);
その後、10万件のテストデータを users および orders テーブルに挿入します(詳細SQLは記事末尾に記載)。
クエリ①:インデックスを使った高速検索
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 50000;
実行計画出力(要約)
-> Index lookup on orders using idx_user_id (user_id=50000)
   (cost=0.35 rows=1)
   (actual time=0.0119..0.0119 rows=0 loops=1)
- インデックス使用:idx_user_id
 - 実行時間:0.012 ms(高速)
 - 実測件数:0(データなし)
 - 推定 vs 実測:rows=1(予測)とrows=0(実測)でズレ
 
クエリ②:JOIN+GROUP BY の集計処理
EXPLAIN ANALYZE
SELECT u.id, u.name, COUNT(o.id) AS total_orders
FROM users u
INNER JOIN orders o
    ON u.id = o.user_id
WHERE u.id BETWEEN 1000 AND 2000
GROUP BY u.id;
実行計画出力(要約)
-> Group aggregate: count(o.id)  (actual time=0.0707..2.68 rows=647)
    -> Nested loop inner join  (actual time=0.0626..2.5 rows=1091)
        -> Index range scan on u using PRIMARY (actual time=0.0445..0.363 rows=1001)
        -> Covering index lookup on o using idx_user_id (actual time=0.0016..0.00188 rows=1.09 loops=1001)
- インデックス使用:両テーブルで有効
 - 実行時間:約2.6ms
 - JOIN対象:1001人 → 注文あり:647人
 - 高速化要因:Covering Index 使用
 
クエリ③:CASTによるインデックス無効化
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE CAST(user_id AS CHAR) = '50000';
実行計画出力(要約)
-> Filter: (cast(user_id as char) = '50000')  (actual time=36.7..36.7 rows=0)
    -> Table scan on orders  (actual time=1.33..25.6 rows=100000)
- インデックス使用:無効化
 - 実行時間:約36ms(遅い)
 - 原因:CAST により全件スキャンが発生
 
MySQL まとめ
EXPLAIN ANALYZE を使うことで、従来の EXPLAIN では見えなかった実際の実行時間や取得行数、ループ回数を可視化でき、クエリのパフォーマンス改善に大きく貢献します。
今回の3つのクエリを比較すると、以下のような違いが明確に表れました。
| クエリ | インデックス | 実行時間 | 実測件数 | 推定との乖離 | 主な原因 | 
|---|---|---|---|---|---|
クエリ①user_id = 50000 | 
使用 | 0.012ms | 0 | あり(rows=1 → 0) | 統計情報の不一致 | 
クエリ②JOIN + GROUP BY | 
使用 | 2.6ms | 647件(JOIN後) | なし(推定精度良好) | インデックス+カバリング最適 | 
クエリ③CAST(user_id AS CHAR) | 
無効 | 36ms | 0 | なし(全件スキャンで当然) | 関数適用によるインデックス無効化 | 
学びと今後の指針
- **インデックスは適切に設計されても、クエリの書き方次第で簡単に無効化される**ことを常に意識する必要があります。
 - **統計情報が古いと推定件数と実測件数にギャップが生まれ、クエリプランが最適でなくなる**ことがあります。
ANALYZE TABLEによる更新も重要です。 EXPLAIN ANALYZEを用いることで、パフォーマンス改善のボトルネックを論理ではなく実測で判断できるようになります。- 特に 関数(CAST・DATE・SUBSTRING など)をWHERE句で使うとインデックスが無効化されるため、
列 = 値の形式を守ることがベストです。 
開発や運用の現場では「インデックスがあるはずなのに遅い」という場面に何度も出くわします。そのたびに EXPLAIN ANALYZE を使って「本当にインデックスが使われているのか」「何件読んでいるのか」「処理が重くなっている箇所はどこか」を検証できると、データベースの見え方が変わってきます。
MySQL 8.0以降の開発では、EXPLAIN ANALYZEを積極的に活用することをおすすめします。
次回のテーマ
EXPLAIN ANALYSISの効果を発揮させるためにANALYSIS TABLEを紹介しましたが、次回はこれをいじってみたいと思います。
