MySQL EXPLAIN/EXPLAIN ANALYZE 速読チートシート:各列の意味と対処法
データベースのパフォーマンス改善に取り組む際、最初に確認すべきなのがクエリの実行計画です。MySQL ではEXPLAINとEXPLAIN ANALYZEというコマンドを使って、クエリがどのように実行されるかを詳しく分析できます。
しかし、EXPLAIN の出力結果には多くの列があり、初めて見る方は「どこから見ればいいの?」「この値は問題があるの?」と戸惑うことも多いでしょう。本記事では、EXPLAIN/EXPLAIN ANALYZE の各列の意味と、問題発見時の具体的な対処法を速読できる形式でまとめました。
チートシートとして活用できるよう、実務でよく遭遇するパターンと解決策を中心に解説していきますね。
EXPLAIN 速読早見表
本記事の内容を素早く参照できるよう、重要な情報を表形式でまとめました。パフォーマンス問題の診断時にご活用ください。
速読 3 ステップチェックリスト
EXPLAIN の結果を効率的に読み解くための優先順位です。
| ステップ | 確認項目 | チェックポイント | 問題の兆候 | 即座の対処 |
|---|---|---|---|---|
| 1 | type 列 | テーブルアクセス方法 | ALL, index が表示 | インデックス追加を検討 |
| 2 | rows 列 | 処理される推定行数 | 10000 行以上 | インデックス最適化または分割 |
| 3 | Extra 列 | 追加の実行情報 | Using filesort, Using temporary | インデックス追加またはクエリ修正 |
| - | key 列 | 使用されているインデックス | NULL(インデックス未使用) | 適切なインデックスを作成 |
type 列:アクセス方法評価早見表
type列はパフォーマンスの最重要指標です。下に行くほどパフォーマンスが悪化します。
| type 値 | 評価 | 意味 | 処理速度 | 対処の必要性 | 主な対処法 |
|---|---|---|---|---|---|
| system | ★★★ | テーブルに 1 行のみ | 最速 | 不要 | - |
| const | ★★★ | PRIMARY KEY/UNIQUE での等値検索 | 最速 | 不要 | - |
| eq_ref | ★★★ | 結合時に 1 行のみマッチ | 高速 | 不要 | - |
| ref | ★★☆ | 非ユニークインデックスでの検索 | 良好 | 通常不要 | rows が大きい場合は複合インデックス |
| fulltext | ★★☆ | FULLTEXT インデックス使用 | 良好 | 通常不要 | - |
| ref_or_null | ★★☆ | ref + NULL 値検索 | 良好 | 通常不要 | - |
| index_merge | ★☆☆ | 複数インデックスのマージ | 注意 | 要確認 | 複合インデックスに統合 |
| range | ★☆☆ | 範囲検索(BETWEEN, >, < など) | 許容範囲 | 要確認 | rows が大きい場合は条件を狭める |
| index | ★☆☆ | インデックスフルスキャン | 要改善 | 必要 | WHERE 句追加または LIMIT 句 |
| ALL | ☆☆☆ | テーブルフルスキャン | 危険 | 即座に必要 | インデックス追加必須 |
Extra 列:追加情報評価早見表
Extra列には実行方法の詳細が表示されます。複数の値が同時に表示されることもあります。
| Extra 値 | 評価 | 意味 | 対処の必要性 | 主な対処法 |
|---|---|---|---|---|
| Using index | ★★★ | カバリングインデックス使用 | 不要(最良) | - |
| Using where | ★★☆ | WHERE 句でフィルタリング | 通常不要 | type が ALL なら要改善 |
| Using index condition | ★★☆ | Index Condition Pushdown | 不要(良好) | - |
| Using filesort | ★☆☆ | ソート処理が発生 | 必要 | ソート列にインデックス追加 |
| Using temporary | ★☆☆ | 一時テーブル使用 | 必要 | GROUP BY 列にインデックス、クエリ書換え |
| Using join buffer | ★☆☆ | 結合バッファ使用 | 要確認 | 結合キーにインデックス追加 |
| Impossible WHERE | ☆☆☆ | WHERE 条件が常に false | 即座に必要 | クエリロジックを修正 |
| No matching row | ☆☆☆ | 該当行なし | 要確認 | データ確認またはクエリ条件見直し |
rows 値:処理行数判断早見表
rows列の値は、テーブルの総行数に対する割合で評価することが重要です。
| rows の値 | 評価 | 状態 | 対処の必要性 | 主な対処法 |
|---|---|---|---|---|
| 1〜100 | ★★★ | 良好 | 不要 | - |
| 101〜1000 | ★★☆ | 注意が必要 | 要確認 | インデックスの見直し |
| 1001〜10000 | ★☆☆ | 要改善 | 必要 | 複合インデックス、条件の絞り込み |
| 10001〜 | ☆☆☆ | 即改善必須 | 即座に必要 | インデックス追加、クエリ分割、統計更新 |
※ テーブルの総行数が 100 万行以上の場合、基準値は 10 倍程度に調整してください。
問題パターンと対処法早見表
よく遭遇する問題パターンと、その対処法を優先順位順にまとめました。
| 問題パターン | 症状(EXPLAIN 結果) | 優先度 | 推奨対処法 | 効果 |
|---|---|---|---|---|
| テーブルフルスキャン | type: ALL | 高 | WHERE 句の列にインデックス追加 | ★★★ |
| インデックスフルスキャン | type: index | 高 | WHERE 句追加、LIMIT 句で制限 | ★★☆ |
| ソート処理の発生 | Extra: Using filesort | 中 | ORDER BY 列にインデックス追加 | ★★★ |
| 一時テーブル使用 | Extra: Using temporary | 中 | GROUP BY を使用、EXISTS に書換え | ★★☆ |
| 大量行の処理 | rows: 10000 以上 | 高 | 複合インデックス、統計情報更新 | ★★★ |
| 後方一致・部分一致 LIKE | type: ALL + WHERE LIKE '%...' | 高 | FULLTEXT インデックス、検索条件の変更 | ★★★ |
| WHERE 句で関数使用 | type: ALL + WHERE FUNCTION(col) = ... | 高 | 関数を使わない形に書換え | ★★★ |
| IN サブクエリ | Extra: Materialize subquery | 中 | EXISTS または JOIN に書換え | ★★☆ |
| 複数インデックスのマージ | type: index_merge | 中 | 複合インデックスに統合 | ★★☆ |
| カバリングインデックス未使用 | key は使用されるが Extra に Using index なし | 低 | SELECT 句を必要な列のみに限定 | ★☆☆ |
| 統計情報の乖離 | rows と actual rows が 10 倍以上乖離 | 中 | ANALYZE TABLE で統計情報更新 | ★★☆ |
EXPLAIN ANALYZE 実測値の確認ポイント
EXPLAIN ANALYZEで取得できる実測値の確認ポイントです。
| 項目 | 説明 | 確認内容 | 問題の兆候 |
|---|---|---|---|
| actual time | 実際の実行時間(ms) | 開始時間..終了時間 | 100ms 以上は要改善 |
| actual rows | 実際に処理された行数 | 推定 rows との比較 | 推定と 10 倍以上の乖離は要注意 |
| loops | 処理のループ回数 | ネストループ結合の回数 | 1 以外は結合方法を要確認 |
| cost | 推定コスト | 相対的な処理コストの比較 | 他のクエリとの比較に使用 |
この早見表を参照しながら、以降の詳細解説をお読みいただくと、より理解が深まります。
背景
MySQL のクエリ実行プロセス
MySQL が SQL クエリを受け取ると、内部で複数のステップを経て結果を返します。
mermaidflowchart TD
parse["1. SQL解析<br/>(Parser)"] --> optimize["2. 最適化<br/>(Optimizer)"]
optimize --> plan["3. 実行計画生成<br/>(Execution Plan)"]
plan --> execute["4. 実行<br/>(Executor)"]
execute --> result["5. 結果返却"]
style plan fill:#e1f5ff
上記の図が示すように、実行計画の生成は最適化フェーズで行われます。この実行計画こそが EXPLAIN で確認できる内容なのです。
EXPLAIN と EXPLAIN ANALYZE の違い
MySQL には 2 種類の実行計画確認方法があります。
| # | コマンド | 説明 | 実行有無 | 取得できる情報 |
|---|---|---|---|---|
| 1 | EXPLAIN | 実行計画のみ表示 | 実行しない | 予測値(推定行数など) |
| 2 | EXPLAIN ANALYZE | 実行計画+実測値 | 実際に実行 | 実測値(実行時間、実際の行数) |
EXPLAINは実際にクエリを実行せず、オプティマイザの予測だけを表示するため安全ですが、予測が外れていることもあります。一方、EXPLAIN ANALYZEは実際にクエリを実行して実測値を取得するため、より正確ですが本番環境では注意が必要です。
パフォーマンス問題の早期発見が重要な理由
データベースのパフォーマンス問題は、アプリケーション全体のユーザー体験に直結します。
レスポンスタイムが 1 秒増えるごとに、ユーザーの離脱率が大きく上昇するという調査結果もあるんです。特に Web アプリケーションでは、データベースのクエリ実行時間がボトルネックになりやすいため、開発段階から EXPLAIN を活用した分析が欠かせません。
課題
EXPLAIN 出力の複雑さ
EXPLAIN を実行すると、多数の列が表示されます。
sqlEXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
このシンプルなクエリでも、以下のような多くの列が出力されます。
| 列名 | 情報の種類 |
|---|---|
| id | クエリ内の実行順序 |
| select_type | SELECT 文の種類 |
| table | 対象テーブル名 |
| partitions | 使用されるパーティション |
| type | 結合タイプ(重要度 ★★★) |
| possible_keys | 使用可能なインデックス候補 |
| key | 実際に使用されるインデックス |
| key_len | インデックスキーの長さ |
| ref | インデックス検索の参照列 |
| rows | 処理される推定行数(重要度 ★★★) |
| filtered | フィルタリング後の推定割合 |
| Extra | 追加情報(重要度 ★★★) |
どの列を優先的に確認すべきか、どの値が問題なのかを判断するのは、初心者にとって大きなハードルとなっています。
問題パターンの見逃し
実行計画を見ても、「これが良いのか悪いのか」が分からないケースが多々あります。
mermaidflowchart LR
explain["EXPLAIN実行"] --> check["結果を確認"]
check --> question["?<br/>これは問題?"]
question -->|判断できない| skip["スルー"]
skip --> performance["パフォーマンス<br/>問題発生"]
style question fill:#ffe0e0
style performance fill:#ff8080
典型的な問題パターンを知らないと、深刻なパフォーマンス問題を見逃してしまう危険性があるんですね。
対処法の選択肢が多すぎる問題
パフォーマンス問題を発見しても、解決方法は多岐にわたります。
インデックスを追加すべきか、クエリを書き直すべきか、テーブル設計を見直すべきか——選択肢が多すぎて、どれから手をつけるべきか迷ってしまいます。各問題パターンに対する効果的な対処法を体系的に把握する必要があるでしょう。
解決策
EXPLAIN 速読の 3 ステップアプローチ
効率的に EXPLAIN を読み解くには、優先順位をつけたアプローチが有効です。
mermaidflowchart TD
start["EXPLAIN実行"] --> step1["ステップ1:<br/>typeをチェック"]
step1 --> step2["ステップ2:<br/>rowsをチェック"]
step2 --> step3["ステップ3:<br/>Extraをチェック"]
step3 --> decision{"問題発見?"}
decision -->|Yes| fix["対処実施"]
decision -->|No| ok["OK"]
style step1 fill:#fff4e0
style step2 fill:#fff4e0
style step3 fill:#fff4e0
このアプローチに従えば、重要な問題を見逃すことなく、効率的に分析できます。
ステップ 1:type 列の確認と評価
type列は、テーブルへのアクセス方法を示す最重要指標です。
type 列の値とパフォーマンス評価
| # | type 値 | 評価 | 意味 | パフォーマンス |
|---|---|---|---|---|
| 1 | system | ★★★ | テーブルに 1 行のみ | 最速 |
| 2 | const | ★★★ | PRIMARY KEY または UNIQUE 索引での検索 | 最速 |
| 3 | eq_ref | ★★★ | 結合時に 1 行のみマッチ | 高速 |
| 4 | ref | ★★☆ | 非ユニーク索引での検索 | 良好 |
| 5 | fulltext | ★★☆ | FULLTEXT 索引使用 | 良好 |
| 6 | ref_or_null | ★★☆ | ref + NULL 値検索 | 良好 |
| 7 | index_merge | ★☆☆ | 複数インデックスのマージ | 注意 |
| 8 | range | ★☆☆ | 範囲検索 | 許容範囲 |
| 9 | index | ★☆☆ | インデックスフルスキャン | 要改善 |
| 10 | ALL | ☆☆☆ | テーブルフルスキャン | 危険 |
問題のある type 値とその対処法
ALL が表示された場合
sql-- 問題のあるクエリ例
EXPLAIN SELECT * FROM orders
WHERE customer_name = 'Yamada';
このクエリでtype: ALLが表示された場合の対処法です。
sql-- 対処法1:適切なインデックスを追加
CREATE INDEX idx_customer_name
ON orders(customer_name);
インデックスを追加することで、typeがrefに改善されます。
sql-- 対処法2:WHERE句の条件を見直す
-- 関数を使うとインデックスが効かない
-- NG例
WHERE YEAR(order_date) = 2024
-- OK例
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01'
関数を使わない形に書き換えることで、インデックスが有効になるんですね。
index が表示された場合
sql-- インデックスフルスキャンが発生する例
EXPLAIN SELECT id FROM orders;
type: indexは、インデックス全体をスキャンしている状態です。
sql-- 対処法1:必要な行だけを取得するようWHERE句を追加
SELECT id FROM orders
WHERE status = 'completed';
-- 対処法2:LIMIT句で取得行数を制限
SELECT id FROM orders
ORDER BY created_at DESC
LIMIT 100;
取得する行を絞り込むことで、パフォーマンスが大幅に改善されます。
ステップ 2:rows 列の確認と評価
rows列は、MySQL が処理すると予測している行数を示します。
rows 値の判断基準
mermaidflowchart TD
check["rowsの値を確認"] --> small{"100行以下?"}
small -->|Yes| good["良好"]
small -->|No| medium{"1000行以下?"}
medium -->|Yes| caution["注意が必要"]
medium -->|No| large{"10000行以下?"}
large -->|Yes| warning["要改善"]
large -->|No| critical["即改善必須"]
style good fill:#d4edda
style caution fill:#fff3cd
style warning fill:#f8d7da
style critical fill:#ff8080
ただし、テーブルの総行数によって評価は変わります。100 万行のテーブルで 1000 行なら優秀ですが、100 行のテーブルで 1000 行なら明らかに異常です。
rows 値が大きい場合の対処法
対処法 1:インデックスの追加または最適化
sql-- 現状確認
EXPLAIN SELECT * FROM products
WHERE category = 'electronics'
AND price > 10000;
-- rows: 50000 と表示された場合
複合インデックスを作成します。
sql-- 複合インデックスの作成
CREATE INDEX idx_category_price
ON products(category, price);
インデックスの列順序は、等値条件(=)を先に、範囲条件(>, <)を後にするのが基本です。
対処法 2:統計情報の更新
sql-- テーブルの統計情報を更新
ANALYZE TABLE products;
統計情報が古いと、オプティマイザの推定が不正確になります。定期的な更新が重要ですね。
対処法 3:クエリの分割
sql-- 大量の行を処理するクエリ
-- NG例
UPDATE orders SET status = 'archived'
WHERE created_at < '2020-01-01';
-- rows: 500000
大量更新は、バッチ処理に分割します。
sql-- OK例:バッチ処理に分割
-- 1回あたり1000件ずつ処理
UPDATE orders SET status = 'archived'
WHERE created_at < '2020-01-01'
AND status != 'archived'
LIMIT 1000;
これを繰り返し実行することで、ロックの影響を最小限にできます。
ステップ 3:Extra 列の確認と対処
Extra列には、実行計画に関する追加情報が表示されます。
頻出する Extra 値と評価
| # | Extra 値 | 評価 | 意味 | 対処必要性 |
|---|---|---|---|---|
| 1 | Using index | ★★★ | カバリングインデックス使用 | 最良 |
| 2 | Using where | ★★☆ | WHERE 句でフィルタリング | 通常 |
| 3 | Using index condition | ★★☆ | Index Condition Pushdown | 良好 |
| 4 | Using filesort | ★☆☆ | ファイルソート発生 | 要改善 |
| 5 | Using temporary | ★☆☆ | 一時テーブル使用 | 要改善 |
| 6 | Using join buffer | ★☆☆ | 結合バッファ使用 | 要確認 |
| 7 | Impossible WHERE | ☆☆☆ | WHERE 条件が矛盾 | 要修正 |
| 8 | No matching row | ☆☆☆ | 該当行なし | 要確認 |
Using filesort への対処
Using filesortは、ソート処理がメモリまたはディスク上で実行されることを示します。
sql-- Using filesortが発生する例
EXPLAIN SELECT * FROM users
ORDER BY last_login_at DESC
LIMIT 10;
ソート対象の列にインデックスを作成します。
sql-- 対処法:ソート列にインデックスを追加
CREATE INDEX idx_last_login
ON users(last_login_at DESC);
降順ソートの場合は、DESCを指定したインデックスが効果的です。
sql-- 複数列でソートする場合
EXPLAIN SELECT * FROM users
ORDER BY status, last_login_at DESC;
複合インデックスを作成します。
sql-- ソート順序に合わせた複合インデックス
CREATE INDEX idx_status_login
ON users(status ASC, last_login_at DESC);
ソート順序を含めたインデックス定義により、パフォーマンスが劇的に向上します。
Using temporary への対処
Using temporaryは、結果を一時テーブルに格納していることを示します。
sql-- Using temporaryが発生する例
EXPLAIN SELECT DISTINCT category
FROM products
WHERE price > 1000;
対処法を見ていきましょう。
sql-- 対処法1:GROUP BYに書き換え
SELECT category
FROM products
WHERE price > 1000
GROUP BY category;
sql-- 対処法2:サブクエリの最適化
-- NG例
SELECT u.* FROM users u
WHERE u.id IN (
SELECT DISTINCT user_id FROM orders
);
EXISTS に書き換えます。
sql-- OK例
SELECT u.* FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
);
EXISTSは、最初にマッチした時点で評価を終了するため、より効率的なんです。
EXPLAIN ANALYZE で実測値を確認
EXPLAIN ANALYZEを使うと、予測と実測の乖離を確認できます。
sql-- EXPLAIN ANALYZEの実行例
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE status = 'pending'
AND created_at > '2024-01-01';
出力例の見方を解説します。
text-> Filter: ((orders.status = 'pending') and (...))
(cost=250.00 rows=100) (actual time=0.05..2.5 rows=85 loops=1)
-> Table scan on orders
(cost=250.00 rows=1000) (actual time=0.03..2.3 rows=1000 loops=1)
重要な指標の読み方
| 項目 | 説明 | 確認ポイント |
|---|---|---|
| cost | 推定コスト | 相対的な比較に使用 |
| rows | 推定行数 | actual rows と比較 |
| actual time | 実際の実行時間(ms) | 開始..終了の形式 |
| actual rows | 実際の処理行数 | rows との乖離を確認 |
| loops | ループ回数 | 1 以外なら要注意 |
推定行数と実際の行数に大きな乖離がある場合は、統計情報の更新が必要です。
sql-- 統計情報を更新
ANALYZE TABLE orders;
具体例
例 1:テーブルフルスキャンの改善
実務でよく遭遇する問題を見ていきましょう。
問題のあるクエリ
sql-- ユーザー検索のクエリ
SELECT * FROM users
WHERE email LIKE '%@example.com';
EXPLAIN を実行します。
sqlEXPLAIN SELECT * FROM users
WHERE email LIKE '%@example.com';
EXPLAIN 結果の分析
| 項目 | 値 | 評価 |
|---|---|---|
| type | ALL | ☆☆☆(危険) |
| rows | 100000 | 全行スキャン |
| Extra | Using where | フィルタリングのみ |
前方一致でないLIKE検索では、インデックスが使用されません。
対処法の実装
sql-- 対処法1:前方一致に変更できる場合
SELECT * FROM users
WHERE email LIKE 'user@%';
ビジネス要件が許せば、検索条件を変更するのが最も効果的です。
sql-- 対処法2:全文検索インデックスを使用
ALTER TABLE users
ADD FULLTEXT INDEX ft_email(email);
sql-- 全文検索での検索
SELECT * FROM users
WHERE MATCH(email) AGAINST('example.com' IN BOOLEAN MODE);
全文検索インデックスにより、部分一致検索も高速化できるんですね。
例 2:結合クエリの最適化
複数テーブルの結合は、パフォーマンス問題の温床です。
問題のあるクエリ
sql-- 注文情報とユーザー情報を結合
SELECT o.*, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending';
EXPLAIN で確認します。
sqlEXPLAIN SELECT o.*, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending';
EXPLAIN 結果の問題点
mermaidflowchart LR
orders["ordersテーブル<br/>(type: ALL)<br/>rows: 50000"] --> join["結合処理"]
users["usersテーブル<br/>(type: eq_ref)<br/>rows: 1"] --> join
join --> result["結果"]
style orders fill:#ff8080
style users fill:#d4edda
ordersテーブルがフルスキャンになっているのが問題です。
段階的な改善
ステップ 1:WHERE 句の列にインデックス追加
sqlCREATE INDEX idx_status
ON orders(status);
sql-- 改善後のEXPLAIN確認
EXPLAIN SELECT o.*, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending';
改善後の結果です。
| テーブル | type | rows | 改善度 |
|---|---|---|---|
| orders | ref | 500 | 100 倍改善 |
| users | eq_ref | 1 | 良好 |
ステップ 2:カバリングインデックスの活用
sql-- よく使う列を含む複合インデックス
CREATE INDEX idx_status_user
ON orders(status, user_id, created_at);
sql-- 必要な列だけを取得するようクエリ修正
SELECT o.created_at, o.total_amount,
u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending';
カバリングインデックスにより、Extra: Using indexが表示され、テーブルへのアクセスが不要になります。
例 3:サブクエリの最適化
サブクエリは便利ですが、パフォーマンス問題の原因になりやすいです。
問題のあるクエリ
sql-- 未払いの注文があるユーザーを取得
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM orders
WHERE status = 'unpaid'
);
EXPLAIN ANALYZE で実測値を確認します。
sqlEXPLAIN ANALYZE
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM orders
WHERE status = 'unpaid'
);
問題の特定
text-> Filter: (users.id in (...))
(cost=1250.00 rows=500) (actual time=0.5..150.2 rows=450 loops=1)
-> Table scan on users
(cost=250.00 rows=5000) (actual time=0.02..1.5 rows=5000 loops=1)
-> Materialize subquery
(cost=1000.00 rows=1000) (actual time=148.5..148.5 rows=1000 loops=1)
サブクエリが一時テーブルにマテリアライズされ、時間がかかっています。
対処法の実装
sql-- JOINに書き換え
SELECT DISTINCT u.*
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'unpaid';
さらに改善します。
sql-- EXISTSを使った書き換え(重複排除不要)
SELECT u.*
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.status = 'unpaid'
);
改善結果の確認
sqlEXPLAIN ANALYZE
SELECT u.*
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.status = 'unpaid'
);
改善後のパフォーマンス比較です。
| 方式 | 実行時間 | 改善率 |
|---|---|---|
| IN (サブクエリ) | 150.2ms | - |
| INNER JOIN | 45.3ms | 70%改善 |
| EXISTS | 25.1ms | 83%改善 |
EXISTSが最も効率的な結果となりました。
例 4:GROUP BY と ORDER BY の最適化
集計クエリもパフォーマンスに影響します。
問題のあるクエリ
sql-- カテゴリ別の売上集計
SELECT category, COUNT(*) as cnt, SUM(price) as total
FROM products
WHERE status = 'active'
GROUP BY category
ORDER BY total DESC;
EXPLAIN で確認しましょう。
sqlEXPLAIN SELECT category, COUNT(*) as cnt, SUM(price) as total
FROM products
WHERE status = 'active'
GROUP BY category
ORDER BY total DESC;
EXPLAIN 結果の問題点
| 項目 | 値 | 問題点 |
|---|---|---|
| type | ALL | フルスキャン |
| Extra | Using where; Using temporary; Using filesort | 3 重苦 |
一時テーブルとファイルソートの両方が発生しています。
対処法の実装
ステップ 1:WHERE 句と GROUP BY 用の複合インデックス
sqlCREATE INDEX idx_status_category
ON products(status, category);
これにより、type: rangeに改善され、Using temporaryが解消されます。
ステップ 2:ソートの最適化
sql-- 計算列でのソートは避けられないため
-- インデックスは効かない
-- 代替案:必要な行数だけ取得
SELECT category, COUNT(*) as cnt, SUM(price) as total
FROM products
WHERE status = 'active'
GROUP BY category
ORDER BY total DESC
LIMIT 20;
LIMITを追加することで、ソート対象を絞り込めます。
ステップ 3:サマリーテーブルの活用
sql-- 集計結果を保存するテーブル
CREATE TABLE category_summary (
category VARCHAR(100) PRIMARY KEY,
product_count INT,
total_price DECIMAL(10,2),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
sql-- 定期的に集計結果を更新
INSERT INTO category_summary
(category, product_count, total_price)
SELECT category, COUNT(*), SUM(price)
FROM products
WHERE status = 'active'
GROUP BY category
ON DUPLICATE KEY UPDATE
product_count = VALUES(product_count),
total_price = VALUES(total_price),
updated_at = CURRENT_TIMESTAMP;
リアルタイム性が不要なら、事前集計が最も効果的なんですね。
まとめ
EXPLAIN/EXPLAIN ANALYZE は、MySQL のパフォーマンスチューニングに欠かせないツールです。本記事で紹介した速読アプローチをまとめます。
速読 3 ステップの振り返り
mermaidflowchart TD
start["EXPLAIN実行"] --> check1{"type列確認"}
check1 -->|ALL/index| fix1["インデックス追加<br/>クエリ修正"]
check1 -->|ref以上| check2{"rows列確認"}
check2 -->|大きい| fix2["統計更新<br/>インデックス最適化"]
check2 -->|小さい| check3{"Extra列確認"}
check3 -->|filesort/temporary| fix3["インデックス追加<br/>クエリ書き換え"]
check3 -->|問題なし| done["完了"]
fix1 --> verify["EXPLAIN再実行"]
fix2 --> verify
fix3 --> verify
verify --> start
style fix1 fill:#ffe0e0
style fix2 fill:#ffe0e0
style fix3 fill:#ffe0e0
style done fill:#d4edda
この図が示すように、問題発見 → 対処 → 再確認のサイクルを回すことが重要です。
重要ポイントの再確認
EXPLAIN の各列で特に注目すべきポイントをまとめました。
| 列名 | 最優先チェック項目 | 危険信号 | 理想的な状態 |
|---|---|---|---|
| type | アクセス方法 | ALL, index | const, eq_ref, ref |
| rows | 処理行数 | 10000 以上 | 100 以下 |
| Extra | 追加処理 | Using filesort, Using temporary | Using index |
| key | 使用インデックス | NULL | インデックス名表示 |
この表をチートシートとして、日々の開発で活用してください。
対処法の優先順位
パフォーマンス問題を発見したときの対処優先順位です。
- インデックスの追加・最適化:最も効果が高く、リスクが低い
- クエリの書き換え:ロジックを変えずに構文を最適化
- 統計情報の更新:定期的なメンテナンスとして実施
- テーブル設計の見直し:大規模な改修が必要な場合の最終手段
段階的に改善していくことで、安全にパフォーマンスを向上できます。
実務での活用ポイント
開発フローに組み込む方法をご紹介します。
新しいクエリを書いたら必ず EXPLAIN を実行する習慣をつけましょう。特に、結合やサブクエリを含むクエリは要注意です。本番環境へのデプロイ前に、EXPLAIN ANALYZEで実測値を確認することで、予期しないパフォーマンス問題を未然に防げます。
また、定期的に遅いクエリログ(slow query log)を確認し、問題のあるクエリを EXPLAIN で分析する運用フローを確立すると、継続的な改善が可能になりますね。
EXPLAIN の読み方をマスターすることで、データベースのパフォーマンス問題に素早く対応できるようになります。本記事をチートシートとして活用し、日々の開発に役立てていただければ幸いです。
関連リンク
より深く学びたい方向けの公式ドキュメントと有用なリソースをご紹介します。
- MySQL 8.0 Reference Manual - EXPLAIN Statement - EXPLAIN 構文の公式ドキュメント
- MySQL 8.0 Reference Manual - EXPLAIN Output Format - EXPLAIN 出力形式の詳細解説
- MySQL 8.0 Reference Manual - Optimizing Queries - クエリ最適化の総合ガイド
- MySQL 8.0 Reference Manual - Optimization and Indexes - インデックス最適化の詳細
- Use The Index, Luke! - インデックスと SQL パフォーマンスの包括的ガイド(多言語対応)
これらのリソースを参考に、さらなるパフォーマンスチューニングの知識を深めていきましょう。
articleMySQL EXPLAIN/EXPLAIN ANALYZE 速読チートシート:各列の意味と対処法
articleMySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現
articleMySQL Hash Join vs Nested Loop 実測:結合選択度で変わる最適解
articleMySQL ERROR 1449 対策:DEFINER 不明でビューやトリガーが壊れた時の復旧手順
articleMySQL InnoDB 内部構造入門:Buffer Pool/Undo/Redo を俯瞰
articleMySQL アラート設計としきい値:レイテンシ・エラー率・レプリカ遅延の基準
articleJest の層別テスト設計:単体/契約/統合をディレクトリで整然と運用
articleMySQL EXPLAIN/EXPLAIN ANALYZE 速読チートシート:各列の意味と対処法
articleMotion(旧 Framer Motion)スクロール進行度マッピング早見表:offset・range・transform の定番式
articleGitHub Copilot と設計ガイドラインの同期:Conventional Commits/ADR/Rulebook 連携
articleMistral 使い方入門:要約・説明・翻訳・書き換えの基礎プロンプト 20 連発
articleGitHub Actions のジョブ分割設計:needs と outputs でデータを安全に受け渡す
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来