MySQL优化器追踪(Optimizer Trace)详解
1.1 Optimizer Trace简介
1.1.1 引入背景
在MySQL中,查询优化器负责解析SQL语句,并根据成本估算选择最优执行计划。然而,对于复杂查询,有时优化器的决策过程难以理解,尤其是出现:
索引选择错误
连接顺序不合理
成本估算偏差
为了解决这些问题,MySQL从5.6版本引入了 Optimizer Trace 功能,用于追踪优化器的内部决策过程,以 JSON 格式输出详细执行信息。
官方文档链接:
MySQL 8.0 Reference Manual - Optimizer Trace
1.1.2 Optimizer Trace 与 EXPLAIN 的区别
特性 | EXPLAIN | Optimizer Trace |
---|---|---|
输出内容 | 查询执行计划 | 优化器内部决策详细信息(JSON) |
适用场景 | 查看执行顺序、索引使用情况 | 分析优化器为何选择特定执行计划 |
输出格式 | 表格 | JSON |
层级信息 | 平面 | 层次化(join_preparation、join_optimization等) |
调试价值 | 较低 | 高,可定位优化器决策逻辑问题 |
可以把 EXPLAIN 理解为“最终计划的快照”,而 Optimizer Trace 则是“优化器的思考过程记录”。
1.1.3 适用场景
复杂多表 JOIN 查询调试:理解优化器如何选择连接顺序
索引选择异常:查看优化器为何未使用期望索引
成本估算异常:分析行数估算、过滤率计算
调优验证:确认优化器是否采纳了调优建议
1.1.4 示例代码:开启 Optimizer Trace 并执行简单查询
环境:MySQL 8.0,单机测试
-- 开启 Optimizer Trace
SET optimizer_trace="enabled=on";-- 创建测试表
CREATE TABLE employees (id INT PRIMARY KEY,name VARCHAR(50),department_id INT,INDEX idx_dept(department_id)
);INSERT INTO employees VALUES
(1,'Alice',1),
(2,'Bob',2),
(3,'Charlie',1);-- 执行简单查询
SELECT * FROM employees WHERE department_id = 1;-- 查看 Optimizer Trace
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G-- 关闭 Optimizer Trace
SET optimizer_trace="enabled=off";
预期结果:
INFORMATION_SCHEMA.OPTIMIZER_TRACE
返回 JSON 结构JSON 包含
steps
、join_order
、costs
等字段可以看到优化器如何决定使用
idx_dept
索引
1.2 工作原理与核心价值
1.2.1 Optimizer Trace 的工作阶段
Optimizer Trace 会在查询优化阶段对优化器的决策流程进行追踪,主要包括以下几个阶段(Stage):
join_preparation
功能:初始化 JOIN 查询的基本信息
内容:
表数量、别名信息
每个表的访问方式(全表扫描、索引访问)
相关统计信息(行数、索引基数)
作用:为后续优化器选择最优 JOIN 顺序做准备
join_optimization
功能:选择最优连接顺序和访问方法
内容:
每个 JOIN 的可能访问方式(access path)
成本估算(cost estimation)
排序选择(order of tables)
作用:记录优化器如何计算每种执行方案的代价并选择最优方案
final_plan
功能:生成最终执行计划
内容:
确定访问索引
确定连接类型(Nested Loop、Block Nested Loop、Hash Join 等)
预估扫描行数和代价
作用:把优化器的“思考结果”形成可执行计划
注意:每个阶段都会输出 JSON 结构,包含详细的决策信息,包括成本(cost)、扫描行数(rows)、使用索引(access_method)等。
1.2.2 JSON 输出结构解析
Optimizer Trace 输出主要字段说明:
{"trace": [{"step": 1,"phase": "join_preparation","table": "employees","access_method": "ALL","rows": 3},{"step": 2,"phase": "join_optimization","join_order": ["employees"],"cost": 0.03,"chosen_access": "index(idx_dept)"},{"step": 3,"phase": "final_plan","plan": [{"table": "employees","access_method": "index","key": "idx_dept","rows_examined": 2}]}]
}
字段说明:
字段 | 说明 |
---|---|
step | 优化器执行步骤序号 |
phase | 优化阶段(join_preparation、join_optimization、final_plan) |
table | 当前操作的表 |
access_method | 表访问方式(ALL 表示全表扫描,index 表示索引访问) |
rows | 预估扫描行数 |
join_order | 优化器尝试的表连接顺序 |
cost | 优化器对执行方案的代价估算 |
chosen_access | 优化器最终选择的访问方式 |
plan | 最终执行计划详细信息 |
key | 使用的索引 |
rows_examined | 实际预计扫描行数 |
通过这个 JSON 输出,开发者可以 逐步追踪优化器的每个决策,并分析为什么某些索引未被使用、JOIN 顺序为何不合理。
1.2.3 Optimizer Trace 的核心价值
可视化优化器决策过程
对比 EXPLAIN,Trace 不仅展示最终计划,还展示选择路径和成本比较。
定位索引选择问题
能够分析为什么优化器没有选择期望索引,便于调整统计信息或优化 SQL。
分析成本估算偏差
Trace 提供每一步的成本和行数估算,方便对比实际执行情况。
调优验证
调整索引或 SQL 后,可通过 Trace 验证优化器是否采纳建议。
结合 EXPLAIN 使用
EXPLAIN 展示最终计划,Trace 展示优化器思路,二者结合可以完整理解查询执行逻辑。
1.2.4 示例代码:分析复杂查询的 Trace 输出
环境:MySQL 8.0
-- 开启 Trace
SET optimizer_trace='enabled=on';-- 创建测试表
CREATE TABLE departments (id INT PRIMARY KEY,name VARCHAR(50)
);CREATE TABLE employees (id INT PRIMARY KEY,name VARCHAR(50),department_id INT,INDEX idx_dept(department_id)
);INSERT INTO departments VALUES (1,'HR'),(2,'IT');
INSERT INTO employees VALUES (1,'Alice',1),(2,'Bob',2),(3,'Charlie',1);-- 执行多表 JOIN 查询
SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.id = 1;-- 查看 Trace
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G-- 关闭 Trace
SET optimizer_trace='off';
分析步骤:
查看
join_preparation
阶段,确认优化器识别了两个表及索引信息。在
join_optimization
阶段,分析优化器尝试的连接顺序和代价比较。在
final_plan
阶段,确认最终选择了idx_dept
索引访问 employees 表,并输出预估扫描行数。对比 EXPLAIN 输出,验证 Trace 的决策与最终执行计划一致性。
.3 配置参数详解
Optimizer Trace 依赖 MySQL 系统变量来控制开启、输出内容和调试精度。主要参数包括:
1.3.1 optimizer_trace
作用:控制 Optimizer Trace 功能开关
类型:字符串(枚举
enabled=on|off
或 JSON 配置)默认值:
enabled=off
修改方法:
-- 开启 Trace SET optimizer_trace='enabled=on';-- 关闭 Trace SET optimizer_trace='enabled=off';
JSON 形式:
SET optimizer_trace='{"enabled": true,"trace_objects": "all","max_elements": 1000 }';
trace_objects
:指定追踪对象,可选all
、optimizer
max_elements
:JSON 输出中最大元素数,超过会被截断
1.3.2 optimizer_trace_features
作用:控制 Trace 输出的内容粒度
类型:字符串(逗号分隔)
默认值:
"basic"
可选值:
basic
:基本信息(表、访问方法、成本)io
:包含 I/O 成本memory
:包含内存使用估算all
:包含全部详细信息
修改方法:
SET optimizer_trace_features='all';
1.3.3 max_optimizer_trace_elements
作用:限制 Trace 输出的最大元素数量
类型:整数
默认值:
1000
修改方法:
SET max_optimizer_trace_elements=2000;
影响:如果 Trace 输出超过限制,JSON 会被截断,分析不完整。
1.3.4 注意事项
系统级变量 vs 会话级变量
SET GLOBAL optimizer_trace='...'
:修改全局,重启后生效SET SESSION optimizer_trace='...'
:只对当前会话有效
与其他参数的冲突
开启
optimizer_trace
时,如果max_optimizer_trace_elements
太小,复杂查询的 Trace 会被截断输出 JSON 大量元素可能导致查询慢,因此只在调试环境开启
对性能的影响
开启 Trace 会增加优化器计算开销
生产环境应谨慎开启,仅用于分析问题
1.3.5 示例:调整参数并观察 Trace 输出差异
环境:MySQL 8.0,测试库
-- 创建测试表
CREATE TABLE test_employees (id INT PRIMARY KEY,name VARCHAR(50),dept_id INT,INDEX idx_dept(dept_id)
);INSERT INTO test_employees VALUES (1,'Alice',1),(2,'Bob',2),(3,'Charlie',1);-- 开启 Trace 并设置输出全部详细信息
SET optimizer_trace='enabled=on';
SET optimizer_trace_features='all';
SET max_optimizer_trace_elements=5000;-- 执行查询
SELECT * FROM test_employees WHERE dept_id=1;-- 查看 Trace
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G-- 调整参数只输出 basic
SET optimizer_trace_features='basic';-- 再次执行查询查看差异
SELECT * FROM test_employees WHERE dept_id=1;-- 关闭 Trace
SET optimizer_trace='enabled=off';
预期效果:
optimizer_trace_features='all'
:输出包含 I/O、内存估算等完整信息optimizer_trace_features='basic'
:只输出表、访问方法、行数和成本通过对比,可以清晰看到不同参数对 Trace 输出内容的影响
1.3.6 小结
optimizer_trace
:开启/关闭 Traceoptimizer_trace_features
:控制输出粒度max_optimizer_trace_elements
:控制最大输出元素数调整参数可以帮助在不同调试场景下获取所需信息,同时避免不必要的性能开销
1.4 使用方法与调试流程
Optimizer Trace 的使用主要包括四个步骤:开启 Trace → 执行查询 → 查看 Trace → 分析与验证。下面逐步讲解。
1.4.1 步骤 1:开启 Optimizer Trace
基本开启方法(会话级):
SET optimizer_trace='enabled=on';
指定输出详细程度(可选):
SET optimizer_trace_features='all'; -- 输出所有信息,包括 I/O 和内存估算 SET max_optimizer_trace_elements=5000; -- 避免复杂查询输出被截断
关闭 Trace:
SET optimizer_trace='enabled=off';
注意:生产环境中建议仅在调试场景开启,以避免性能开销。
1.4.2 步骤 2:执行需要分析的查询
示例:MySQL 8.0
CREATE TABLE employees (id INT PRIMARY KEY,name VARCHAR(50),department_id INT,INDEX idx_dept(department_id)
);CREATE TABLE departments (id INT PRIMARY KEY,name VARCHAR(50)
);INSERT INTO departments VALUES (1,'HR'),(2,'IT');
INSERT INTO employees VALUES (1,'Alice',1),(2,'Bob',2),(3,'Charlie',1);-- 执行复杂 JOIN 查询
SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.id = 1;
查询执行过程中,优化器会根据统计信息计算多个执行方案,并记录在 Trace 中
Trace 会捕捉每个阶段(join_preparation、join_optimization、final_plan)的决策
1.4.3 步骤 3:查看 Trace 输出
查询 Trace:
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G;
输出分析:
trace
字段为 JSON,包含所有优化器决策步骤关键字段:
phase
:优化阶段table
:操作的表access_method
:表访问方式rows
:预估扫描行数cost
:优化器成本估算chosen_access
:最终选择访问方式plan
:最终执行计划详情
结合 JSON 格式化工具,可清晰看到优化器每一步决策过程。
1.4.4 步骤 4:分析与验证
与 EXPLAIN 对比
EXPLAIN SELECT e.name, d.name FROM employees e JOIN departments d ON e.department_id = d.id WHERE d.id = 1\G;
EXPLAIN 提供最终执行计划,包含访问索引、连接类型、扫描行数
Optimizer Trace 提供优化器选择该计划的完整逻辑
分析决策过程
查看
join_optimization
阶段:哪些访问方案被尝试
各方案成本估算
为什么最终选择当前方案
查看
final_plan
阶段:验证访问索引是否符合预期
验证扫描行数与成本是否合理
调试索引选择错误示例
假设优化器未使用期望索引:
-- 查询未使用索引
SELECT * FROM employees WHERE department_id=1;-- Trace 显示 access_method=ALL(全表扫描)
-- 分析可能原因:
-- 1. 索引统计信息不准确
-- 2. 表行数过少,优化器认为全表扫描成本低
-- 调整统计信息
ANALYZE TABLE employees;-- 再次执行查询,观察 Trace 输出是否选择 idx_dept 索引
通过对比 Trace 输出,可以清楚看到优化器为何选择或忽略某个索引,从而指导索引优化或 SQL 调整。
1.4.5 小结
开启 Trace → 执行查询 → 查看输出 → 分析决策 是标准流程
Trace 能够详细记录优化器每个阶段的决策,包括访问方式、成本估算、连接顺序
与 EXPLAIN 配合使用,可以同时掌握最终计划与优化器思路
适用于调试复杂查询、索引选择错误、成本估算偏差等问题
1.5 典型场景分析
场景1:索引选择错误
1.5.1.1 问题描述
查询条件明确,但优化器未使用预期索引
导致全表扫描或性能下降
示例查询(MySQL 8.0):
SELECT * FROM employees WHERE department_id = 1;
1.5.1.2 Trace 输出分析
{"trace": [{"phase": "join_preparation","table": "employees","access_method": "ALL","rows": 3},{"phase": "final_plan","plan": [{"table": "employees","access_method": "ALL","rows_examined": 3}]}]
}
分析:
access_method=ALL
表示全表扫描优化器认为全表扫描成本低于索引访问,可能由于:
表数据量小
索引统计信息过时
1.5.1.3 调优建议
-- 更新表统计信息
ANALYZE TABLE employees;-- 再次执行查询,观察 Trace 输出
SELECT * FROM employees WHERE department_id=1;-- 预期 Trace 输出:
-- access_method=index
-- 使用 idx_dept 索引
场景2:成本估算偏差
1.5.2.1 问题描述
优化器选择的执行计划成本估算与实际执行成本差距大
可能导致非最优查询计划
示例:
SELECT * FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.id IN (1,2);
1.5.2.2 Trace 输出分析
{"phase": "join_optimization","join_order": ["departments","employees"],"cost": 0.05,"chosen_access": "index(idx_dept)"
}
分析:
优化器选择先扫描
departments
再扫描employees
如果统计信息不准确,实际扫描行数可能比估算大
Trace 提供每个阶段成本和行数估算,有助于定位问题
1.5.2.3 调优建议
-- 更新表统计信息
ANALYZE TABLE employees;
ANALYZE TABLE departments;-- 调整查询逻辑或提示优化器
SELECT /*+ JOIN_ORDER(d,e) */ e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.id IN (1,2);
场景3:子查询优化问题
1.5.3.1 问题描述
子查询导致性能不佳,优化器未转换为 JOIN
Trace 可以帮助分析子查询执行计划
示例:
SELECT name
FROM employees
WHERE department_id IN (SELECT id FROM departments WHERE name='HR'
);
1.5.3.2 Trace 输出分析
{"phase": "join_optimization","subquery": {"type": "IN","rows": 1,"access_method": "ALL"},"final_plan": {"employees": {"access_method": "index", "rows_examined": 2}}
}
分析:
子查询使用全表扫描,可能导致性能下降
Trace 显示优化器未将子查询转换为半连接(semi-join)
1.5.3.3 调优建议
-- 转换为 JOIN
SELECT e.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.name='HR';
Trace 输出应显示:
access_method=index
子查询被消除,性能提升明显
场景4:临时表/文件排序问题
1.5.4.1 问题描述
查询涉及 ORDER BY、GROUP BY 或 DISTINCT
优化器使用临时表或文件排序,影响性能
示例:
SELECT department_id, COUNT(*)
FROM employees
GROUP BY department_id
ORDER BY COUNT(*) DESC;
1.5.4.2 Trace 输出分析
{"phase": "final_plan","plan": [{"table": "employees","access_method": "index","using_temporary": true,"using_filesort": true}]
}
分析:
using_temporary=true
,using_filesort=true
表示临时表 + 文件排序对大表可能导致性能瓶颈
1.5.4.3 调优建议
-- 使用覆盖索引或调整查询
CREATE INDEX idx_dept_count ON employees(department_id);-- 执行查询并观察 Trace 输出
-- 预期不再使用文件排序,性能提升
1.5.5 小结
Optimizer Trace 能清晰展示优化器每一步决策
通过 Trace 可定位:
索引未使用
成本估算不准
子查询优化未生效
临时表或文件排序影响性能
调优流程:
查看 Trace
分析决策逻辑
调整索引或 SQL
验证 Trace 输出变化