前言
EXPLAIN 是 MySQL 效能調校的「聽診器」。透過分析執行計劃,我們能洞悉優化器如何存取資料、使用何種索引、是否產生臨時表或檔案排序。本文系統解析 EXPLAIN 12 個輸出欄位含義、type 存取類型層級、Extra 關鍵提示、EXPLAIN ANALYZE (8.0.18+) 實際執行統計,並附實戰判讀邏輯與優化決策樹。
一、EXPLAIN 基本語法與變體
-- 傳統格式 (表格)
EXPLAIN SELECT * FROM users WHERE id = 1;
-- JSON 格式 (8.0+,含更多成本資訊)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1;
-- 實際執行統計 (8.0.18+,真實耗時、回傳行數、迴圈次數)
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;
-- 追蹤優化器決策 (5.6+,需開啟 optimizer_trace)
SET SESSION optimizer_trace="enabled=on";
SELECT * FROM users WHERE id = 1;
SELECT * FROM information_schema.optimizer_trace;
二、12 個標準輸出欄位詳細解析
| 欄位 | 含義 | 關鍵判讀重點 |
| id | SELECT 識別符,查詢序列號 | 相同 id 為同一查詢組;id 越大越先執行 (子查詢);id 相同按順序執行 |
| select_type | 查詢類型 | SIMPLE (無 UNION/子查詢)、PRIMARY (最外層)、UNION (UNION 第 2+ 個)、DEPENDENT UNION、UNION RESULT、SUBQUERY、DEPENDENT SUBQUERY (相關子查詢)、DERIVED (衍生表/FROM 子查詢)、MATERIALIZED (物化子查詢) |
| table | 存取的表名 (或衍生表別名 ) | 關聯查詢中顯示各表存取順序 (驅動表 → 被驅動表) |
| partitions | 匹配的分區 (分區表) | NULL=非分區表;顯示具體分區名 = 分區剪枝生效 |
| type | 存取類型 (核心指標,由優到劣) | 見下節詳細層級表 |
| possible_keys | 可能用到的索引 | NULL = 無可用索引,**需檢查 WHERE/JOIN/ORDER BY 欄位是否有索引** |
| key | 實際使用的索引 | NULL = **全表掃描或未命中索引**,極度危險 |
| key_len | 使用索引的位元組長度 | 判斷複合索引用了幾欄:長度 = Σ(欄位長度) + NULL位(每欄1位) + 變長欄位長度資訊(2位)。越短越好 (精確性前提下) |
| ref | 索引查找的比較值來源 | const (常數)、col_name (關聯欄位)、func (函數結果)、NULL (無比較值) |
| rows | 預估掃描行數 (統計資訊推算) | **越小越好**;與實際行數差距大 = 統計資訊過期需 ANALYZE;乘積 = 總預估成本 |
| filtered | 預估符合條件行數百分比 (5.7+) | rows × filtered/100 = 實際參與 JOIN/回傳行數;< 10% 表示條件過濾性差 |
| Extra | 額外執行資訊 (關鍵優化線索) | 見下節關鍵值對照表 |
三、type 存取類型層級 (由優到劣)
| 類型 | 描述 | 典型場景 | 效能評級 |
| system | 表僅 1 行 (系統表) | mysql.time_zone 等 | ★★★★★ |
| const | 主鍵/唯一鍵等值查詢,最多 1 行 | WHERE id=1 (id 為 PK/UK) | ★★★★★ |
| eq_ref | 唯一索引等值 JOIN,每行匹配 1 行 | JOIN ON pk = pk | ★★★★★ |
| ref | 非唯一索引等值查詢/JOIN | WHERE status=1 (status 有索引) | ★★★★☆ |
| ref_or_null | ref + 額外搜尋 NULL | WHERE col=1 OR col IS NULL | ★★★★☆ |
| index_merge | 索引合併優化 (多索引 UNION/INTERSECT) | WHERE idx1=1 OR idx2=2 | ★★★☆☆ |
| unique_subquery | IN 子查詢轉等值查找 (主鍵) | WHERE id IN (SELECT pk FROM t) | ★★★★☆ |
| index_subquery | IN 子查詢轉索引查找 (非唯一) | WHERE col IN (SELECT idx FROM t) | ★★★☆☆ |
| range | 索引範圍掃描 | WHERE id > 100, BETWEEN, IN, LIKE 'abc%' | ★★★☆☆ |
| index | 全索引掃描 (掃描整棵 B+Tree) | SELECT indexed_col FROM t (覆蓋索引) | ★★☆☆☆ |
| ALL | 全表掃描 (最差) | 無索引、索引失效、統計資訊錯誤 | ★☆☆☆☆ |
四、Extra 關鍵值對照表 (優化紅綠燈)
| Extra 值 | 含義 | 效能影響 | 優化方向 |
| Using index | 覆蓋索引:查詢欄位全在索引中,無需回表 | 🟢 極佳 | 設計覆蓋索引 |
| Using where | Server 層過濾 (Storage Engine 返回後再篩選) | 🟡 正常 | 下推條件至存儲引擎 (Index Condition Pushdown) |
| Using index condition | ICP (Index Condition Pushdown):索引層過濾,減少回表 | 🟢 優秀 | 確保 5.6+ 開啟 ICP (預設 ON) |
| Using filesort | 檔案排序:無法用索引排序,需臨時表/記憶體排序 | 🔴 **嚴重** | 調整索引順序滿足 ORDER BY、避免範圍查詢後排序、Limit 省略排序 |
| Using temporary | 臨時表:GROUP BY/DISTINCT/UNION/ORDER BY 不同欄位需物化 | 🔴 **嚴重** | 索引滿足 GROUP BY/ORDER BY、避免 SELECT *、子查詢改 JOIN |
| Using join buffer | Block Nested Loop JOIN (無索引關聯) | 🔴 嚴重 | 加上 JOIN 條件索引、改用 Hash Join (8.0.18+) |
| Select tables optimized away | 聚合查詢優化:MIN/MAX/COUNT 直接讀索引極值 | 🟢 極佳 | 確保索引存在 |
| Distinct | 找到第一匹配行後停止掃描 | 🟡 正常 | - |
| Not exists | LEFT JOIN 優化:找到匹配行後不再搜尋右表 | 🟢 優秀 | - |
| Range checked for each record | 無理想索引,每行組合重新評估索引選擇 | 🔴 極差 | 建立複合索引覆蓋 WHERE 條件 |
| Impossible WHERE | WHERE 條件永假 (如 1=0) | 🟢 極佳 | - |
| Impossible HAVING | HAVING 條件永假 | 🟢 極佳 | - |
五、EXPLAIN ANALYZE 實戰優勢 (8.0.18+)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id=123 ORDER BY created_at DESC LIMIT 10;
-- 輸出關鍵實際值 (TREE 格式)
-> Limit: 10 row(s) (actual time=0.5..1.2 rows=10 loops=1)
-> Sort: orders.created_at DESC (actual time=0.5..1.1 rows=1000 loops=1)
-> Filter: (orders.user_id = 123) (actual time=0.1..0.8 rows=1000 loops=1)
-> Index range scan on orders using idx_user_id (actual time=0.05..0.3 rows=1000 loops=1)
(cost=100 rows=1000)
核心優勢:actual time (啟動/結束毫秒)、actual rows (實際行數)、loops (迴圈次數)、cost (優化器成本)。**直接對比 estimated rows vs actual rows 發現統計資訊偏差**。
六、實戰判讀邏輯與優化決策樹
步驟 1:看 type → 定性
ALL → **全表掃描**,必須加索引或改寫 SQL。
index → **全索引掃描**,若非覆蓋索引則需加 WHERE 條件限制範圍。
range/ref/eq_ref/const → **索引生效**,進入步驟 2。
步驟 2:看 key/key_len/ref → 定準
key=NULL → 索引未命中,檢查隱式轉型、函數包裹、最左前綴違反。
key_len 過短 → 複合索引未用全欄,檢查 WHERE/JOIN/ORDER BY 順序。
ref 為 func 或 NULL → 可能有函數/運算導致索引失效。
步驟 3:看 rows/filtered → 定量
rows 巨大 (>10萬) → 掃描成本高,需縮小範圍或分區。
filtered 低 (<10%) → 過濾性差,考慮複合索引包含過濾欄位。
步驟 4:看 Extra → 定紅綠燈
- 🔴 Using filesort / Using temporary / Using join buffer → **立即優化**,參考對照表方向。
- 🟢 Using index / Using index condition / Not exists → **良好模式**,保持。
- 🟡 Using where → 常態,若配合
ref 且 rows 小可接受。
步驟 5:多表 JOIN 看 id/table 順序 → 定驅動表
- id 相同、table 順序 = JOIN 順序 (驅動表 → 被驅動表)。
- 驅動表應為**結果集最小、過濾性最強**的表 (通常有高選擇性 WHERE 條件)。
- 被驅動表**必須有 JOIN 條件索引** (ref/eq_ref)。
七、常見誤區與避坑
| 誤區 | 真相 | 正確做法 |
| EXPLAIN 顯示用了索引就一定快 | type=index (全索引掃描) 比 ALL 略好但仍慢;key_len 短未用全複合索引 | 綜合判讀 type、key_len、rows、Extra |
| rows 是實際掃描行數 | rows 是**預估值** (統計資訊推算),實際行數見 EXPLAIN ANALYZE | 定期 ANALYZE TABLE 更新統計資訊 |
| Using where 代表用了 WHERE 索引 | Using where 只是 Server 層過濾,不代表索引命中 | 看 type、key、key_len 確認索引使用 |
| 複合索引順序隨意 | 嚴格遵循最左前綴:等值 → 範圍 → 排序 | 索引設計時同步規劃 WHERE/JOIN/ORDER BY |
| EXPLAIN 不會執行 SQL | EXPLAIN ANALYZE **會真實執行** (含副作用),生產謹慎用 | 生產用 EXPLAIN FORMAT=JSON 或備份庫跑 ANALYZE |
八、總結:EXPLAIN 速查卡片
┌─────────────────────────────────────────────────────────────────┐
│ EXPLAIN 判讀三步走 │
├─────────────────────────────────────────────────────────────────┤
│ 1️⃣ type: 是否 ALL/index? → 是 → 加索引/改寫 │
│ 2️⃣ key: 是否 NULL? → 是 → 檢查隱式轉型/函數/最左前綴 │
│ 3️⃣ Extra: 是否 🔴 filesort/temporary/join_buffer? → 是 → 優化 │
│ 是否 🟢 Using index? → 否 → 嘗試覆蓋索引 │
├─────────────────────────────────────────────────────────────────┤
│ 關鍵公式: 總成本 ≈ Σ(rows_i × filtered_i/100) │
│ 目標: type ≥ ref, key ≠ NULL, rows 最小, Extra 綠燈 │
└─────────────────────────────────────────────────────────────────┘