MySQL 慢SQL深度分析:EXPLAIN 与 optimizer_trace 全解析
在 MySQL 性能优化体系中,慢SQL分析是核心技能。本文将从 EXPLAIN 执行计划解读 到 optimizer_trace 优化器追踪,构建完整的慢查询诊断方法论。
一、EXPLAIN 基础与核心字段总览
1.1 EXPLAIN 使用方法
基础语法:
sql
-- 分析 SELECT 语句
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- 获取 JSON 格式详细计划(推荐用于复杂分析)
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 100;
-- 分析 UPDATE/DELETE(MySQL 5.6+)
EXPLAIN UPDATE orders SET status = 'paid' WHERE user_id = 100;
关键输出字段:
sql
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
分析优先级 :type → key → rows → Extra,这四个字段决定了查询性能基线。
二、type 字段详解:访问类型优化路径
type 字段表示表连接类型,是判断查询效率的核心指标,性能从好到差排序如下:
2.1 最优类型:system 与 const
system :表中仅有一条数据,是 const 类型的特例
sql
-- 示例:系统配置表仅一行
EXPLAIN SELECT * FROM config WHERE id = 1;
const:使用主键或唯一索引进行等值匹配,最多返回一条记录
sql
-- 示例:主键等值查询
EXPLAIN SELECT * FROM users WHERE id = 100; -- type: const
性能特征 :这些类型代表单表访问 ,性能最优,查询时间为常量级。
2.2 高效类型:eq_ref 与 ref
eq_ref :连接查询中,驱动表通过唯一索引访问被驱动表
sql
-- 示例:employees 表通过主键关联 salaries 表
EXPLAIN
SELECT e.*, s.salary
FROM employees e
JOIN salaries s ON e.emp_no = s.emp_no; -- s 表 type: eq_ref
ref :使用非唯一索引进行等值匹配,可能返回多条记录
sql
-- 示例:通过普通索引查询
EXPLAIN SELECT * FROM orders WHERE user_id = 100; -- type: ref
优化目标 :核心业务 SQL 至少达到 ref 级别。
2.3 中等类型:range 与 index
range :索引范围扫描,常用于 >, <, BETWEEN, IN 等操作
sql
-- 示例:时间范围查询
EXPLAIN SELECT * FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31';
index:全索引扫描,遍历整个索引树,比全表扫描快
sql
-- 示例:查询索引列(无需回表)
EXPLAIN SELECT user_id FROM orders; -- 只查索引列
注意 :index 虽然比 ALL 快,但对大表仍是性能隐患。
2.4 最差类型:ALL(全表扫描)
ALL :全表扫描,必须优化
sql
-- 示例:无索引或索引失效
EXPLAIN SELECT * FROM orders WHERE YEAR(create_time) = 2024; -- type: ALL
EXPLAIN SELECT * FROM orders WHERE status = 'shipped'; -- status 无索引
优化路径:
- 添加索引:为 WHERE 条件列创建索引
- 避免函数 :
YEAR(create_time)→create_time >= '2024-01-01' - 强制索引 :
FORCE INDEX(idx_create_time)
2.5 type 优化检查清单
| type 类型 | 性能等级 | 是否需要优化 | 典型场景 |
|---|---|---|---|
| system | ★★★★★ | 无需 | 单行配置表 |
| const | ★★★★★ | 无需 | 主键等值查询 |
| eq_ref | ★★★★★ | 无需 | JOIN 主键关联 |
| ref | ★★★★☆ | 可接受 | 普通索引等值查询 |
| range | ★★★☆☆ | 观察 | 小范围查询 |
| index | ★★☆☆☆ | 建议优化 | 全索引扫描 |
| ALL | ★☆☆☆☆ | 必须优化 | 全表扫描 |
三、ref、rows、filtered 字段深度解读
3.1 ref:索引引用列
含义:显示哪些列或常量被用于索引查找
sql
-- 示例:常量引用
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- ref: const
-- 示例:多表连接
EXPLAIN
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id;
-- orders 表 ref: const (若 user_id = 100)
-- users 表 ref: db.o.user_id (引用 orders 表的 user_id)
3.2 rows:预估扫描行数
核心作用 :MySQL 优化器预估需要读取的行数。值越小越好,但需注意:
- 预估值:基于统计信息,可能不准确
- 优化目标 :rows < 1000 为佳,rows > 10000 需警惕
统计信息更新:
sql
-- 当 rows 预估偏差大时,更新统计信息
ANALYZE TABLE orders;
实战案例:
sql
-- 慢查询:rows=980000
SELECT * FROM orders WHERE user_id=1000 AND status='shipped' ORDER BY create_time DESC LIMIT 10;
-- EXPLAIN 分析:rows 过大,说明索引选择性差
-- 优化:创建 (user_id, status, create_time) 复合索引
3.3 filtered:条件过滤率
含义:表示返回结果的行占扫描行数的百分比
sql
-- 示例:扫描100行,返回10行,filtered=10%
EXPLAIN SELECT * FROM users WHERE age > 25 AND city = 'Beijing';
优化意义 :filtered 值低说明索引选择性差,需考虑:
- 更换索引列:将高选择性列放前面
- 覆盖索引:避免回表
四、Extra 字段:性能优化的"宝藏提示"
Extra 字段包含查询执行的额外信息,是性能诊断的关键线索。
4.1 高危信号:必须优化
Using filesort:无法使用索引排序,需额外排序操作
sql
-- 触发场景:ORDER BY 非索引列或索引列顺序错误
EXPLAIN SELECT * FROM orders ORDER BY amount; -- amount 无索引
-- 优化:创建排序索引
ALTER TABLE orders ADD INDEX idx_create_time (create_time);
EXPLAIN SELECT * FROM orders ORDER BY create_time; -- Extra: NULL
Using temporary:需创建临时表存储中间结果
sql
-- 触发场景:DISTINCT、GROUP BY、ORDER BY 无索引
EXPLAIN SELECT DISTINCT user_id FROM orders; -- 无索引
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status; -- status 无索引
-- 优化:添加索引
ALTER TABLE orders ADD INDEX idx_status (status);
4.2 优化提示:建议改进
Using index :使用覆盖索引,无需回表
sql
-- 理想状态
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 100;
-- Extra: Using index (覆盖索引)
Using index condition:使用索引下推(ICP)
sql
-- 复合索引 (name, age)
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age = 10;
-- Extra: Using index condition (age 条件在存储引擎层过滤)
Using MRR:使用多范围读取优化
sql
-- 范围查询
EXPLAIN SELECT * FROM orders WHERE user_id BETWEEN 100 AND 200;
-- Extra: Using MRR (回表时顺序读取)
4.3 Extra 字段优化口诀
| Extra 值 | 含义 | 优化动作 |
|---|---|---|
| Using filesort | 文件排序 | 添加排序索引 |
| Using temporary | 临时表 | 添加 GROUP BY/ORDER BY 索引 |
| Using index | 覆盖索引 | 保持现状,理想状态 |
| Using index condition | 索引下推 | 确认 ICP 开启 |
| Using MRR | 多范围读取 | 确认 MRR 开启 |
| Using where | Server 层过滤 | 考虑索引覆盖 |
| NULL | 正常 | 无需优化 |
五、optimizer_trace:优化器决策黑盒破解
5.1 什么是 optimizer_trace?
optimizer_trace 是 MySQL 5.6 引入的优化器决策追踪工具,记录优化器如何选择执行计划的完整过程。它解决了 EXPLAIN 的局限性:
- EXPLAIN 只展示最终选择的执行计划
- optimizer_trace 展示为什么选这个计划(包括所有备选方案和成本计算)
5.2 开启与配置
会话级开启(推荐):
sql
-- 开启 trace
SET optimizer_trace="enabled=on",end_markers_in_json=on;
-- 执行需要分析的 SQL
SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';
-- 查看 trace 结果
SELECT * FROM information_schema.optimizer_trace \G;
全局开启(慎用):
sql
SET GLOBAL optimizer_trace="enabled=on",end_markers_in_json=on;
核心参数:
sql
-- 控制 trace 内存大小(默认 1MB)
SET optimizer_trace_max_mem_size = 1048576;
-- 限制返回的 trace 条数(默认 1)
SET optimizer_trace_limit = 1;
-- 控制跟踪内容(默认全部开启)
SET optimizer_trace_features = 'greedy_search=on,range_optimizer=on,dynamic_range=on';
5.3 trace 结构解析
trace 以 JSON 格式存储,包含 3 大阶段:
阶段 1:join_preparation(SQL 准备)
json
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `orders`.`id` AS `id` ... from `orders` where ..."
}
]
}
}
作用:展示 SQL 重写后的标准格式,确认优化器接收到的查询。
阶段 2:join_optimization(核心优化阶段)
子阶段 2.1:condition_processing(条件处理)
json
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "((`orders`.`user_id` = 100) AND (`orders`.`status` = 'shipped'))",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(multiple equal(100, `orders`.`user_id`) ...)"
}
]
}
}
作用:展示条件重写过程(等值传播、常量传播)。
子阶段 2.2:rows_estimation(行数估算)
json
{
"rows_estimation": [
{
"table": "`orders`",
"range_analysis": {
"table_scan": {
"rows": 1000000,
"cost": 101000
},
"potential_range_indexes": [
{
"index": "idx_user_id",
"usable": true,
"key_parts": ["user_id"]
}
]
}
}
]
}
作用 :对比全表扫描 vs 索引扫描的成本,揭示为什么选择某个索引。
子阶段 2.3:considered_execution_plans(执行计划选择)
json
{
"considered_execution_plans": [
{
"plan_prefix": [],
"table": "`orders`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "ref",
"index": "idx_user_id",
"rows": 100,
"cost": 120.5,
"chosen": true
},
{
"access_type": "ALL",
"rows": 1000000,
"cost": 101000,
"chosen": false
}
]
},
"cost_for_plan": 120.5,
"rows_for_plan": 100,
"chosen": true
}
]
}
作用 :展示所有候选计划的成本对比,最终选择 cost 最小的计划。
阶段 3:join_execution(执行阶段)
json
{
"join_execution": {
"select#": 1,
"steps": []
}
}
5.4 实战案例:索引选择之谜
问题:为什么有时 MySQL 不选"最优"索引?
场景 :表 orders 有索引 idx_user_id 和 idx_status,查询:
sql
SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';
EXPLAIN 结果:
sql
-- 可能使用了 idx_status,而非 idx_user_id
type: ref
key: idx_status
rows: 5000
optimizer_trace 分析:
sql
-- 开启 trace
SET optimizer_trace="enabled=on";
-- 执行查询
SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';
-- 查看 trace
SELECT * FROM information_schema.optimizer_trace \G;
trace 关键信息:
json
{
"potential_range_indexes": [
{
"index": "idx_user_id",
"rows": 100, // user_id=100 过滤后约 100 行
"cost": 150.0
},
{
"index": "idx_status",
"rows": 5000, // status='shipped' 过滤后约 5000 行
"cost": 120.0 // 但 cost 更低!
}
]
}
真相 :优化器认为 idx_status 的 cost 更低 (可能因为 idx_user_id 的回表成本高),但实际 idx_user_id 更优。
解决方案:
sql
-- 方案1:强制使用索引
SELECT * FROM orders FORCE INDEX(idx_user_id) WHERE user_id = 100 AND status = 'shipped';
-- 方案2:创建复合索引
ALTER TABLE orders ADD INDEX idx_user_status(user_id, status);
六、慢SQL分析完整流程
6.1 四步诊断法
Step 1:识别慢SQL
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 阈值1秒
-- 查看慢SQL日志
mysqldumpslow -s t /var/log/mysql/slow.log | head -20;
Step 2:EXPLAIN 初步分析
sql
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';
-- 重点关注:type, key, rows, Extra
Step 3:optimizer_trace 深度追踪(当 EXPLAIN 无法解释时)
sql
SET optimizer_trace="enabled=on";
-- 执行SQL
-- 查看 trace,分析优化器决策
Step 4:针对性优化
- 索引优化:添加/修改索引
- SQL重写:避免函数、转换条件
- 结构优化:分表分区
- 参数调优 :调整
join_buffer_size,read_rnd_buffer_size
6.2 实战案例:从慢查询到优化
原始慢SQL:
sql
-- 执行时间:5秒
SELECT * FROM orders
WHERE DATE(create_time) = '2024-01-01'
ORDER BY id
LIMIT 10;
Step 1:EXPLAIN 分析
sql
EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01' ORDER BY id LIMIT 10;
-- 结果:
-- type: ALL(全表扫描)
-- key: NULL(未使用索引)
-- rows: 1000000
-- Extra: Using where
问题定位:
DATE(create_time)导致索引失效ORDER BY id产生 filesort
Step 2:optimizer_trace 验证
sql
-- 开启 trace
SET optimizer_trace="enabled=on";
-- trace 显示:优化器放弃 idx_create_time 索引,因为函数操作
"condition_processing": {
"transformation": "func_date_conversion",
"index_unusable": true
}
Step 3:优化方案
sql
-- 优化后SQL(执行时间:50ms)
SELECT * FROM orders
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00'
ORDER BY id
LIMIT 10;
-- 添加复合索引
ALTER TABLE orders ADD INDEX idx_ctime_id(create_time, id);
Step 4:验证优化效果
sql
EXPLAIN SELECT * FROM orders
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00'
ORDER BY id
LIMIT 10;
-- 结果:
-- type: range
-- key: idx_ctime_id
-- rows: 100
-- Extra: Using index condition
-- 性能提升:5秒 → 50ms(100倍)
七、高级技巧与避坑指南
7.1 optimizer_trace 实战技巧
场景1:多个索引的选择困惑
sql
-- 表有 idx_a, idx_b, idx_c,查询 WHERE a=1 AND b=2 AND c=3
-- EXPLAIN 显示使用了 idx_c,但不确定为何不用 idx_ab
-- 使用 trace 查看 cost 计算
"considered_execution_plans": [
{"access_type": "ref", "index": "idx_ab", "cost": 200, "chosen": false},
{"access_type": "ref", "index": "idx_c", "cost": 150, "chosen": true}
]
场景2:JOIN 顺序优化
sql
-- 三表 JOIN,优化器选择了错误的驱动表
-- trace 中查看 table_dependencies 和 considered_plans
7.2 常见误区
误区1:rows 越小越好
- 真相 :rows 是预估值,
filtered列同样重要。可能扫描100行过滤出10行(filtered=10%),不一定比扫描500行过滤出400行(filtered=80%)更优。
误区2:有索引就一定会用
- 真相 :优化器基于 cost 决策,可能因回表成本高 、数据分布不均 等原因放弃索引。此时需用
FORCE INDEX或优化索引设计。
误区3:EXPLAIN 显示 Using index 就完美
- 真相 :需确认是否 Using filesort 或 Using temporary,覆盖索引不能解决所有问题。
7.3 慢SQL优化口诀
索引失效三宗罪:函数计算、隐式转换、左模糊
排序优化两法宝:索引覆盖、最左前缀
连接查询要记牢:小表驱动、索引关联
trace 分析三步走:开启执行、查看 JSON、对比 cost
八、总结:从表象到本质的优化思维
| 工具 | 作用 | 适用场景 | 输出价值 |
|---|---|---|---|
| EXPLAIN | 执行计划快照 | 日常快速诊断 | type/key/rows/Extra |
| optimizer_trace | 优化器决策过程 | 索引选择异常、复杂查询 | 成本计算、备选方案 |
| 慢查询日志 | 性能监控 | 定位慢SQL | 执行时间、扫描行数 |
核心方法论:
- 先监控:通过慢日志定位慢SQL
- 再快照:用 EXPLAIN 获取执行计划
- 后追踪:用 optimizer_trace 破解决策黑盒
- 终验证:优化后重复分析,形成闭环
真正的性能优化不是"调参数",而是理解 MySQL 优化器的决策逻辑,让 SQL、索引、数据分布三者协同工作。掌握 EXPLAIN 和 optimizer_trace,就相当于拥有了慢SQL分析的"CT扫描仪"和"病理报告"。