1. はじめに
データベースパフォーマンスの最適化に不可欠な「実行計画」
Webアプリケーションや業務システムにおいて、データベースのパフォーマンスは全体のレスポンスに直結する重要な要素です。特にMySQLを使用している場合、クエリの効率性を測るために「実行計画(Execution Plan)」を把握することが不可欠です。従来から利用されてきた EXPLAIN
コマンドは、SQL文の実行前にその計画を表示し、開発者にとって重要な手がかりを提供してきました。
MySQL 8.0で導入された「EXPLAIN ANALYZE」
MySQL 8.0.18から新たに登場した EXPLAIN ANALYZE
は、従来の EXPLAIN
を一歩進化させた強力なツールです。これまでの EXPLAIN
が「理論的な計画」にとどまっていたのに対し、EXPLAIN ANALYZE
は実際にクエリを実行し、実行時間や行数などの「実測データ」を提供します。これにより、より正確なボトルネックの特定や、クエリ最適化の効果検証が可能となります。
なぜ EXPLAIN ANALYZE
が重要なのか
たとえば、JOINの順序、インデックスの有無、フィルターのかかり具合などは、実行時間に大きな影響を及ぼします。EXPLAIN ANALYZE
を使えば、SQL文の実行結果を目視で確認しながら、どの部分に無駄があるか、どこを最適化すべきかを判断できます。これは特に大規模データや複雑なクエリを扱う現場では、欠かせない分析手法といえるでしょう。
本記事の目的と想定読者
この記事では、MySQLの EXPLAIN ANALYZE
の基本的な使い方から、出力結果の読み解き方、実践的な最適化手法までを段階的に解説していきます。対象読者は、MySQLを日常的に使用している開発者やインフラ担当者、パフォーマンス改善に興味を持つエンジニアです。初心者の方にも理解しやすいように、用語解説や具体例を交えながら進めていきますので、安心して読み進めてください。
2. EXPLAIN
とEXPLAIN ANALYZE
の違い
EXPLAIN
の役割と基本的な使い方
MySQLの EXPLAIN
は、SQL文(特にSELECT文)がどのように実行されるかを事前に知るための分析ツールです。インデックスの使用有無、テーブルの結合順序、検索範囲などの実行計画を確認することができます。
たとえば、以下のように使います。
EXPLAIN SELECT * FROM users WHERE age > 30;
このようなコマンドを実行すると、MySQLはそのクエリを実行せずに、どのように処理される予定かを表形式で表示します。出力には、使用されるインデックス(key)、アクセス方法(type)、見積もり行数(rows)などが含まれます。
EXPLAIN ANALYZE
の役割と特長
一方で、MySQL 8.0.18以降に導入された EXPLAIN ANALYZE
は、実際にクエリを実行したうえで、実測値に基づいた実行計画を表示する新機能です。これにより、従来の EXPLAIN
では見えなかった「処理にかかった実時間」や「実際に処理された行数」が確認できるようになりました。
実行例は以下の通りです:
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 30;
このコマンドでは、MySQLは実際にクエリを実行し、出力として以下のような情報を返します。
- 実行プランの各ステップにかかった時間(e.g.
0.0022 sec
) - 実際に読み取った行数(
rows
) - 処理のネスト構造(TREE形式を使うことで可視化が容易)
両者の主な違いのまとめ
項目 | EXPLAIN | EXPLAIN ANALYZE |
---|---|---|
実行の有無 | 実行しない | 実際にクエリを実行する |
提供情報 | 実行前の推定情報 | 実行後の実測情報 |
使用用途 | インデックスや結合順序の確認 | 実際のパフォーマンス分析 |
MySQLバージョン | 古くから存在 | MySQL 8.0.18以降 |
どちらを使うべきか?
- クエリの構造を素早く確認したい場合は
EXPLAIN
- クエリの処理時間や実行コストを具体的に把握したい場合は
EXPLAIN ANALYZE
という使い分けが基本になります。特にパフォーマンスチューニングの場面では、EXPLAIN ANALYZE
によって「予測」ではなく「実態」に基づいた最適化が可能となるため、非常に有効なツールといえるでしょう。
3. EXPLAIN ANALYZE
の出力形式
出力形式は3種類:TRADITIONAL・JSON・TREE
MySQLの EXPLAIN ANALYZE
は、用途や目的に応じて異なるフォーマットで結果を出力できます。MySQL 8.0以降では、以下の3種類の出力形式が用意されています。
フォーマット名 | 特徴 | 使いやすさ |
---|---|---|
TRADITIONAL | 表形式の従来スタイル。馴染みやすい | 初心者向け |
JSON | 構造化された詳細情報が得られる | 分析ツール連携向け |
TREE | ネスト構造が視覚的にわかる | 中級者以上向け |
それぞれの形式の違いを詳しく見ていきましょう。
TRADITIONAL形式(デフォルト)
TRADITIONALは、従来の EXPLAIN
と似たスタイルの出力で、実行計画を表形式で確認できます。EXPLAIN ANALYZE
をそのまま使うと、基本的にはこの形式で出力されます。
出力例(一部抜粋):
-> Filter: (age > 30) (cost=0.35 rows=10) (actual time=0.002..0.004 rows=8 loops=1)
cost
:推定コストactual time
:実測時間rows
:推定される処理行数(実行前)loops
:ループ回数(特にJOINで重要)
TRADITIONAL形式は人間がパッと見て理解しやすく、初心者や簡易確認に向いています。
JSON形式
より詳細でプログラムからも扱いやすいのがJSON形式です。構造化されており、各ノードの情報がネストされたオブジェクトとして出力されます。
実行コマンド:
EXPLAIN ANALYZE FORMAT=JSON SELECT * FROM users WHERE age > 30;
出力例の一部(整形済):
{
"query_block": {
"table": {
"table_name": "users",
"access_type": "range",
"rows_examined_per_scan": 100,
"actual_rows": 80,
"filtered": 100,
"cost_info": {
"query_cost": "0.35"
},
"timing": {
"start_time": 0.001,
"end_time": 0.004
}
}
}
}
この形式は視覚的には読みにくいですが、データをパースしてツールやダッシュボードに取り込む際には非常に便利です。
TREE形式(読みやすさと構造の可視化)
TREE形式は、クエリの処理構造を木構造で表示する形式で、JOINやサブクエリの処理順序が視覚的に把握できます。
実行コマンド:
EXPLAIN ANALYZE FORMAT=TREE SELECT * FROM users WHERE age > 30;
出力例(簡略化):
-> Table scan on users (actual time=0.002..0.004 rows=8 loops=1)
複雑なクエリでは、以下のようにネストが表示されます:
-> Nested loop join
-> Table scan on users
-> Index lookup on orders using idx_user_id
TREE形式は特にJOINの多いクエリや複雑なネストがあるケースで、処理の流れをつかむのに適しています。
どの形式を使えばよいか?
使用シーン | 推奨フォーマット |
---|---|
初学者で簡単に見たい | TRADITIONAL |
プログラムで解析したい | JSON |
構造やネストを確認したい | TREE |
目的に応じて形式を選び、最も見やすく、解析しやすいスタイルで実行計画を確認しましょう。
4. 実行計画の読み解き方
なぜ実行計画を読む必要があるのか?
MySQLのクエリは、データ量やインデックスの有無によって処理速度が大きく変わります。EXPLAIN ANALYZE
を使って出力された実行計画を正しく読み解くことで、どこに無駄があり、どこを改善すれば良いのかを客観的に判断できるようになります。特に、大量データや複雑な結合処理を行うクエリにおいては、このスキルがパフォーマンスチューニングの要となります。
実行計画の基本構成
EXPLAIN ANALYZE
の出力には、以下のような情報が含まれます(TRADITIONAL形式を基準に解説):
-> Filter: (age > 30) (cost=0.35 rows=10) (actual time=0.002..0.004 rows=8 loops=1)
この1行には、複数の重要な情報が詰まっています。
項目 | 説明 |
---|---|
Filter | 条件句(WHEREなど)に該当するフィルタ処理 |
cost | クエリ実行前の推定コスト |
rows | 推定される処理行数(実行前) |
actual time | 実際にかかった時間(開始〜終了) |
actual rows | 実際に処理された行数 |
loops | この処理が繰り返された回数(ネスト時に重要) |
重要な項目の見方
1. cost
と actual time
cost
はMySQLの内部計算による「コストの見積もり」で、実行速度の相対評価に使われます。actual time
は現実にかかった時間で、性能分析ではこちらがより重要です。
たとえば:
(cost=0.35 rows=100) (actual time=0.002..0.004 rows=100)
このように、推定と実測がほぼ一致していれば、実行計画の精度は高いと考えられます。逆に差が大きければ、統計情報の精度が低い可能性があります。
2. rows
と actual rows
rows
はMySQLが予測した「読む予定の行数」actual rows
は実際に読み取った行数(TRADITIONAL形式では括弧の中に含まれる)
この2つに大きな乖離がある場合、統計情報の更新やインデックスの設計見直しが必要です。
3. loops
loops=1
であれば一度だけの実行ですが、JOINやサブクエリがあると loops=10
や loops=1000
と増えることがあります。この値が大きいほど、ネストされたループによって処理が重くなっている可能性があります。
実行計画のネスト構造を把握する
複数テーブルのJOINがある場合、実行計画はツリー状に表示されます(特にTREE形式で顕著です)。
例:
-> Nested loop join
-> Table scan on users
-> Table scan on orders
問題点
- 両テーブルが全件走査されており、結合コストが高い。
対応策
users.age
にインデックスを貼り、絞り込み処理を先に行うことで、結合対象を減らす。
パフォーマンスのボトルネックを特定するには?
以下のようなポイントに注目すると、ボトルネックが見つかりやすくなります。
- actual timeが長く、rowsも多いノード:処理時間の大部分を占める
- full table scanが発生している箇所:インデックス未使用の可能性
- loopsが多い部分:JOIN順やネストの非効率性を示す
- rowsとactual rowsの乖離:統計の不正確さや過剰なデータアクセス
これらをもとに、次章で紹介する「クエリ最適化」に活かしていくことが重要です。
5. クエリ最適化の実践例
クエリ最適化とは?
クエリ最適化とは、SQL文をより効率的に実行できるように見直す作業を指します。MySQLが内部でどのようにクエリを処理しているか(実行計画)をもとに、インデックスの追加、結合順序の変更、不要な処理の削減などを行います。
ここでは、EXPLAIN ANALYZE
を用いて、実際にどのようにクエリを改善できるのかを、具体的な例とともに紹介します。
例1:インデックスの活用による高速化
最適化前のクエリ
SELECT * FROM users WHERE email = 'example@example.com';
実行計画の一部
-> Table scan on users (cost=10.5 rows=100000) (actual time=0.001..0.230 rows=1 loops=1)
問題点
Table scan
とある通り、全件走査が行われており、データ件数が多いと遅延の原因になります。
対応策:インデックスの追加
CREATE INDEX idx_email ON users(email);
最適化後の実行計画
-> Index lookup on users using idx_email (cost=0.1 rows=1) (actual time=0.001..0.002 rows=1 loops=1)
結果
- 実行時間が大幅に短縮。
- インデックスにより、不要な全件走査を回避。

例2:結合順序の最適化
最適化前のクエリ
SELECT * FROM orders
JOIN users ON orders.user_id = users.id
WHERE users.age > 30;
実行計画の一部
-> Nested loop join
-> Table scan on orders
-> Table scan on users
問題点
- 両テーブルが全件走査されており、結合コストが高い。
対応策
users.age
にインデックスを貼り、絞り込み処理を先に行うことで、結合対象を減らす。
CREATE INDEX idx_age ON users(age);
最適化後の実行計画
-> Nested loop join
-> Index range scan on users using idx_age
-> Index lookup on orders using idx_user_id
結果
- JOIN対象が先に絞り込まれ、全体の処理負荷が減少。
例3:サブクエリの見直し
最適化前のクエリ
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);
問題点
- サブクエリが実行ごとに評価され、パフォーマンスが低下する。
対応策:JOINに書き換え
SELECT DISTINCT users.*
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.total > 1000;
結果
- 実行計画ではJOINが最適化され、インデックスが使われやすくなる。
Before/Afterの比較が鍵
EXPLAIN ANALYZE
を使うことで、最適化の効果を「実測値」で確認できます。改善前後での実行時間や行数の変化を見ることで、単なる「やったつもり」で終わらず、確実なチューニングが可能になります。
最適化における注意点
- 過剰なインデックス追加は逆効果になる場合もある(INSERT/UPDATE性能の低下)。
- 実行計画はデータ量や統計情報に依存するため、環境ごとに確認が必要。
- 一つの最適化で全てが速くなるとは限らない。ボトルネック分析が最優先。
6. 注意点とベストプラクティス
EXPLAIN ANALYZE
利用時の注意点
EXPLAIN ANALYZE
は非常に強力なツールですが、正しく使わないと逆に誤解やトラブルを招く恐れもあります。以下の注意点を意識することで、安全かつ効果的にクエリの分析と最適化が可能になります。
1. 本番環境では安易に実行しない
EXPLAIN ANALYZE
は、クエリを実際に実行するため、更新系のSQL文(INSERT/UPDATE/DELETEなど)を誤って使うと、データが変更されてしまう危険があります。
SELECT
文以外では基本的に使わないようにしましょう。- 本番データでの実行は避け、検証用の環境で行うのが原則です。
2. キャッシュの影響を考慮する
MySQLは同じクエリを繰り返すと、データをキャッシュから返すことがあります。そのため、EXPLAIN ANALYZE
の実行時間が実態と異なることがあります。
対策:
- 実行前にキャッシュをクリア(
RESET QUERY CACHE;
)する。 - 複数回実行して、ばらつきのない平均値を取る。
3. 統計情報を更新することが前提
MySQLはインデックスやカラムの統計情報に基づいて実行計画を立てます。これらが古くなっていると、EXPLAIN
も ANALYZE
も正しい情報を返せません。
特に大量のINSERT/DELETEがあった後は、ANALYZE TABLE
で統計を更新しておきましょう。
ANALYZE TABLE users;
4. インデックスは万能ではない
インデックスを追加することで高速化できるケースは多いですが、インデックスが多すぎると、書き込み系の処理が遅くなるという副作用があります。
また、複合インデックスと単一インデックスの選択も重要です。使用頻度や検索条件に応じて、適切なインデックス設計を心がけましょう。
5. 実行時間だけで判断しない
EXPLAIN ANALYZE
の結果は、あくまでクエリ単体のパフォーマンスを示すものです。アプリケーション全体で見ると、ネットワーク遅延やバックエンド処理など、他の要素がボトルネックになることもあります。
したがって、全体設計の中でクエリの役割を理解したうえでの分析が求められます。
ベストプラクティスまとめ
ポイント | 実施内容 |
---|---|
本番環境の使用注意 | SELECT限定で使い、更新系では使わない |
キャッシュ対策 | 実行前にキャッシュクリア、平均値で判断 |
統計情報の更新 | ANALYZE TABLEで定期的に統計を整備 |
インデックスのバランス設計 | 読み書きの比率を見て、必要最小限に留める |
クエリ単体にとらわれすぎないこと | アプリ全体の中で最適化すべき点を見極める |
7. よくある質問(FAQ)
Q1. EXPLAIN ANALYZE
はどのバージョンから利用できますか?
A.
MySQLの EXPLAIN ANALYZE
は バージョン8.0.18以降 に導入された機能です。8.0より前のバージョンではサポートされていませんので、利用する場合はMySQLのバージョン確認が必要です。
Q2. EXPLAIN ANALYZE
を実行すると、データが変更されることはありますか?
A.
基本的に EXPLAIN ANALYZE
はクエリを実行せずに分析を行うため、SELECT文であればデータの変更は発生しません。
しかし、もし誤って INSERT
や UPDATE
などを使った場合、通常のクエリ同様にデータが変更されてしまいます。
※安全のため、分析には 検証用のテストデータベースで実行することを推奨します。
Q3. EXPLAIN
だけではダメなのですか?
A.EXPLAIN
は実行計画の「推定」を確認するには十分ですが、実際にどれくらい時間がかかったか、どのくらいの行数が処理されたかといった実測値は出力されません。
本格的にクエリチューニングを行いたい場合や、効果検証が必要な場合は、EXPLAIN ANALYZE
の方が有用です。
Q4. 出力される「loops」や「actual time」などの数値は、どの程度正確ですか?
A.actual time
や loops
の数値は、MySQLが内部的に計測した実行時のリアルな情報です。ただし、OSの状況やキャッシュ状態、サーバーの負荷などによって微妙に変動します。
そのため、1回だけで判断せず、複数回の実行で傾向を見るのが理想です。
Q5. 実行結果の「cost」って具体的に何を表しているのですか?
A.cost
はMySQL内部のコストモデルによる推定値であり、CPU・I/Oコストなどを相対的に評価した指標です。単位は実時間(秒)ではないため、あくまで相対的な優劣を見るために使います。
例えば (cost=0.3)
と (cost=2.5)
があれば、後者の方が重い処理と推定されるという意味です。
Q6. JSON形式やTREE形式を使うと、何が便利なのですか?
A.
- JSON形式:構造化されており、プログラムで解析しやすい。自動化ツールやダッシュボードに活用しやすい。
- TREE形式:処理の流れやネスト構造が視覚的に理解しやすく、複雑なクエリやJOINの順序が一目でわかる。
それぞれ用途が異なるため、目的に応じて使い分けるのがポイントです。
Q7. 実行計画を見て改善できなかった場合はどうすれば?
A.
以下のような追加手段を検討すると良いでしょう。
- インデックスの再設計(複合インデックスやカバリングインデックス)
- クエリ自体の書き換え(サブクエリ→JOIN、不要なSELECT列の削除)
- ビューや一時テーブルの活用
- MySQLの構成(buffer size等)の見直し
チューニングは1つの方法だけで完結することは少ないため、複合的に考えることが大切です。