MySQLのEXPLAIN ANALYZEを読むときは、まず「rowsの推定値」と「actual rowsの実測値」、次に「actual time」と「loops」を比べます。EXPLAIN ANALYZEはクエリを実際に実行してTREE形式の実行計画を返すため、単なるEXPLAINより実行時の挙動を確認しやすくなります。ただし実際にSQLを動かすので、本番データでは負荷と対象ステートメントに注意が必要です。
EXPLAIN ANALYZEが分かること
実行計画には「MySQLがどの処理を、どの順番で、何件ほど扱うと見積もったか」が表示されます。EXPLAIN ANALYZEでは、各処理(イテレータ)が実際に返した行数や経過時間、反復回数も表示されるため、見積もりとの差を確認できます。
| 表示項目 | 意味 | 見るべきポイント |
|---|---|---|
| cost | オプティマイザの推定コスト | 実時間msではない |
| rows | 推定される行数 | actual rowsとの差 |
| actual time=a..b | 最初の行と処理完了までの実測時間(ミリ秒) | 時間の大きい処理を探す |
| actual rows | 実際に返した行数 | 推定とどれだけ違うか |
| loops | その処理を実行した回数 | 繰り返しコストを見逃さない |
用語解説:イテレータ
実行計画のTable scan、Filter、Index lookup、Joinなど個々の処理単位。TREEの入れ子で親子関係を表します。
まずSELECTに付けて実行する
MySQL 8.4の例です。検証可能な環境で、まず通常のEXPLAINで実行計画を確認します。その後、実行負荷を把握したうえでEXPLAIN ANALYZEを使用します。
EXPLAIN
SELECT id, customer_id, ordered_at
FROM orders
WHERE customer_id = 42
ORDER BY ordered_at DESC
LIMIT 20;
EXPLAIN ANALYZE FORMAT=TREE
SELECT id, customer_id, ordered_at
FROM orders
WHERE customer_id = 42
ORDER BY ordered_at DESC
LIMIT 20;
ordersはサンプルの仮想テーブル名です。実環境のテーブル・列名に置き換えます。EXPLAIN ANALYZEはSQLを実行します。更新を伴うステートメントへ無条件に使用せず、実行権限・負荷・環境を確認してください。
MySQL 8.4ではEXPLAIN ANALYZEの出力はTREE形式です。FORMAT=JSONまたはFORMAT=TRADITIONALを指定するとエラーになります。
推定rowsとactual rowsの差を読む
表示例のイメージは次の形です。以下の行数と時間は説明用の架空値であり、実データベースで計測した結果ではありません。
-> Filter: (orders.customer_id = 42)
(cost=420 rows=100)
(actual time=0.5..35.0 rows=5000 loops=1)
-> Table scan on orders
(cost=1200 rows=100000)
(actual time=0.3..25.0 rows=100000 loops=1)
ここではFilterが返す行数の見積もりが100件、実測が5000件です。見積もりのずれが大きい場合、統計情報の鮮度や列の値の偏り、絞り込み条件とインデックスの適合を調べる手掛かりになります。
ただし「ずれが大きい=必ずそのSQLが遅い」ではありません。まず実行負荷の大きい処理を把握し、行数差は原因候補を絞り込む指標として使います。
actual timeは親子とloopsも確認する
actual timeは各イテレータで最初の行を返すまでと、処理完了までにかかった時間をミリ秒で示します。loopsが複数ある場合、時間は1回あたりの平均として表示されます。
| 表示 | 読むポイント |
|---|---|
| actual time=0.2..12.0 | 最初の行が返るまで約0.2ms、完了まで約12ms |
| rows=20 loops=1 | その処理が返した行数と反復回数 |
| rows=2 loops=1000 | 反復処理が多い。行数や時間を1回分だけで判断しない |
TREEは入れ子構造です。子処理を含む親処理の時間と子処理の時間を、単純にすべて足してSQL全体の所要時間だと判断しないようにします。ネストしたループではloopsの多い内側のIndex lookupなども確認してください。
Table scanとIndex lookupで判断する
実行計画にTable scanがあると即座に悪いと考えがちですが、テーブルが小さい場合や多くの行を読む必要がある場合、全表走査が妥当なこともあります。まず検索条件で絞った行数と実測時間を確認しましょう。
- WHEREで使う列に適切なインデックスが存在するか確認する。
- ORDER BYとWHEREの組み合わせに合う複合インデックスか考える。
- SELECT *で不要な列を大量に読み込んでいないか確認する。
- LIMITがあっても並び替えや前段の走査に時間が掛かっていないか見る。
- インデックスを増やす前に更新処理・容量・既存クエリへの影響を検証する。
条件がcustomer_idで絞り込み、ordered_atで並び替えなら、(customer_id, ordered_at) の複合インデックスは検討候補です。ただし最適解はテーブル定義・データ分布・実際の実行計画に依存します。
推定値が外れるときの調査順
- 実際の実行環境でSQL条件とパラメータを確認する。
- EXPLAINとEXPLAIN ANALYZEのTREE出力から、大きな行数差・loops・時間を確認する。
- 該当列のインデックス、型、WHERE条件、ORDER BYを確認する。
- 統計情報が古い可能性を検討し、必要に応じてANALYZE TABLEの適用影響を調べる。
- SQLやインデックスを変更するなら、変更前後で同じ入力条件の実測値を比較する。
本番でEXPLAIN ANALYZEを繰り返すと負荷につながる可能性があります。既知の重いSQLは検証環境で再現するか、実行タイミングを管理したうえで確認してください。
関連するSQLの記事
実行計画を読んだ後、SQLの改善手順に進みたい場合はSQLチューニングの全体像が参考になります。またJOINの結合条件がネックなら、基本的なJOINの仕組みを整理すると原因を絞りやすくなります。
(SQL改善の手順については『SQLチューニングとは?遅いSQLを最短で改善する3つの手順』をご参照ください)
(JOINの仕組みについては『SQL JOINの基本から実践まで|INNER・LEFT・RIGHT・OUTER JOINの違いをわかりやすく解説』をご参照ください)
よくある疑問
EXPLAINとEXPLAIN ANALYZEは何が違う?
EXPLAINは主にオプティマイザの実行計画を表示します。EXPLAIN ANALYZEは対象SQLを実行して実測値を加えます。
rowsとactual rowsが違うと必ず遅い?
いいえ。見積もりとの差は調査の手掛かりですが、時間、loops、実際の処理内容と併せて判断します。
EXPLAIN ANALYZEはSELECTだけ?
MySQL 8.4の公式マニュアルでは、SELECTのほか複数テーブルUPDATE・DELETE、TABLE文も対象に含まれます。更新系は実際に処理を動かす点に特に注意してください。
遅いSQLの原因を探るときは、推定rowsとactual rows、actual time、loopsをセットで読むことが基本です。最初からインデックスを追加するのではなく、どのイテレータに時間がかかり、見積もりと実測がどこでずれているか確認するところから始めてください。
まずは検証用のSELECTにEXPLAINを付け、実行計画の読み方を一つずつ確認してみましょう。