EXPLAIN
命令是查询优化的基础工具,但对于复杂的 SQL 查询,EXPLAIN
的输出可能不够详细,难以深入了解优化器的决策过程。 这时,Optimizer Trace (优化器追踪) 就成为了更强大的 “神兵利器”。 Optimizer Trace 能够记录 MySQL 优化器在优化查询过程中 每一步的决策细节,包括成本估算、规则应用、执行计划选择等等,为我们提供了前所未有的 透明度和可控性,是诊断和解决疑难查询性能问题的终极武器。
Optimizer Trace 不仅仅是展示最终的执行计划,而是 完整记录了优化器从接收 SQL 查询到最终选择执行计划的整个过程。 它就像是优化器的 “debug 日志”,将优化器的思考过程 “原原本本” 地呈现出来,让我们能够:
理解优化器的决策过程: 清晰地看到优化器是如何生成候选执行计划、如何进行成本估算、如何应用优化规则、以及最终选择了哪个执行计划。
定位优化瓶颈: 精确地找出优化过程中哪个环节出现了问题,例如,成本估算不准确、规则应用不合理、索引选择错误等等。
验证优化策略: 检验我们的优化思路是否有效,例如,添加索引是否真的被优化器使用,查询改写是否真的提升了性能。
深入学习优化器原理: 通过分析 Optimizer Trace 的输出,可以更深入地理解 MySQL 优化器的内部工作机制,提升自身的优化技能。
Optimizer Trace 的使用步骤主要分为三个阶段:
optimizer_trace
和 end_markers_in_json
系统变量来开启 Optimizer Trace 功能。SET optimizer_trace="enabled=on,one_line=off";
-- 开启 Optimizer Trace, 格式化输出 SET end_markers_in_json=on; -- 在 JSON 输出中添加结束标记 (可选,方便解析)
**optimizer_trace="enabled=on,one_line=off"**
:
enabled=on
: 启用 Optimizer Trace 功能。
one_line=off
: 禁用单行输出,输出格式化的 JSON 结果 (更易读)。 如果设置为 one_line=on
,则输出单行 JSON 字符串。
**end_markers_in_json=on**
: 可选参数,在 JSON 输出的每个阶段添加开始和结束标记,方便程序解析 JSON 结果。
SELECT * FROM orders WHERE order_date >= '2023-01-01' AND customer_id = 123 ORDER BY order_id DESC LIMIT 10;
information_schema.OPTIMIZER_TRACE
表 或 performance_schema.optimizer_trace
表来获取 Optimizer Trace 的详细信息。 MySQL 8及以上版本推荐 information_schema.OPTIMIZER_TRACE
SELECT * FROM information_schema.OPTIMIZER_TRACE\G -- 或者 SELECT * FROM performance_schema.optimizer_trace\G
information_schema.OPTIMIZER_TRACE
** 表 / **performance_schema.optimizer_trace**
表:** 存储 Optimizer Trace 的结果。 每个 SQL 查询的 Trace 信息会存储在该表的一行记录中。 该表包含以下主要列:
QUERY
: 被追踪的 SQL 查询语句。
TRACE
: JSON 格式的 Optimizer Trace 详细信息。
MISSING_BYTES_BEYOND_MAX_MEM_SIZE
: 超出 optimizer_trace_max_mem_size
限制的字节数。
INSUFFICIENT_PRIVILEGES
: 是否因权限不足导致追踪信息不完整。
Optimizer Trace 的输出结果是 JSON 格式的,结构较为复杂,但信息非常丰富。 JSON 结果主要包含以下几个顶层节点 (根据 MySQL 版本和查询类型,节点可能有所不同):
steps
** (优化步骤)* 最核心的节点,包含了优化器在优化查询过程中执行的每一个步骤的详细信息。 steps
是一个 JSON 数组,数组中的每个元素代表一个优化步骤,例如 “join_preparation”、“join_optimization”、“join_execution” 等. **深入分析 ****steps**
节点,是理解优化器决策过程的关键。steps
数组中的每个元素 (优化步骤) 通常包含以下字段:
join_preparation
: 准备阶段, 包含子查询的预处理、条件化简等
subqueries_preparation
: 子查询预处理信息.
join_optimization
: 优化阶段, 核心部分, 包含各种优化决策
condition_processing
: 条件处理, WHERE 条件的化简、常量传递等
table_dependencies
: 表依赖关系分析.
ref_optimizer_key_uses
: ref
类型访问的索引使用情况.
rows_estimation
: 扫描行数和成本估算
potential_range_indexes
: 考虑进行范围扫描的索引.
analyzing_range_alternatives
: 范围扫描索引分析.
considered_execution_plans
: 考虑过的执行计划列表.
attaching_conditions_to_tables
: 将条件附加到表上的操作.
reconsidering_access_paths_for_index_ordering
: 是否重新考虑索引的顺序.
best_access_path
: 优化器选择的最佳访问路径
condition_on_constant_tables
: 常量表上的条件.
making_join_plan
: 生成连接计划.
join_execution
: 执行阶段
using_join_cache
: 连接缓存相关信息
query_block
** (查询块)* 表示查询语句中的一个独立查询块,例如一个 SELECT 语句或一个子查询。
select_id
: 查询块的唯一标识符。
cost_info
: 查询块的成本估算信息,包括:
query_cost
: 整个查询块的预估成本。table
: 查询块涉及的表的信息
table_name
:表名
access_type
: 访问类型
possible_keys
: 可能用到的索引
key
: 实际使用的索引
key_length
: 使用的索引长度
used_key_parts
:使用的索引部分
rows_examined_per_scan
: 预估扫描的行数
rows_produced_per_join
:预估产生的连接行数。
filtered
: 过滤后的行数百分比。
解读 steps
节点中的关键信息
steps
节点是优化的核心, join_optimization
是最重要的部分, 以下是几个关键子步骤的解读:
condition_processing
(条件处理):
original_condition
: 原始的 WHERE 条件。
simplified_conditions
: 化简后的条件。
analyzing_range_alternatives
(范围扫描备选分析):
range_scan_alternatives
: 数组, 每个元素代表一个索引范围扫描方案.
index
: 索引名称
ranges
: 扫描范围.
index_only
: 是否只使用索引(覆盖索引)
rowid_ordered
: 行ID是否有序。
using_mrr
: 是否使用 Multi-Range Read 优化。
index_dives_for_eq_ranges
: 是否使用了索引跳跃扫描
rows
: 预估扫描行数。
cost
: 预估成本
chosen
: 是否选择该索引。analyzing_roworder_intersect
: 分析是否可以使用 rowid 排序的交集.
considered_execution_plans
(考虑的执行计划):
plan_prefix
: 连接的表顺序.
table
: 当前表的详细信息.
access_type
: 访问类型.rows_examined_per_scan
: 预估扫描行数.
rows_produced_per_join
: 连接产生的行数.
cost
: 计划总成本.
chosen
: 是否被选中.
ref_optimizer_key_uses
****:
ref
类型访问索引的详细情况Optimizer Trace 案例分析 (索引选择错误排查)
场景: 某个查询在有索引的情况下仍然执行缓慢,怀疑是优化器没有选择最优索引。
SQL 查询:
SELECT * FROM products WHERE category_id = 5 AND price BETWEEN 100 AND 200;
假设:
products
表有 category_id_idx
(category_id 列索引) 和 price_idx
(price 列索引)。
预期优化器使用复合索引 (category_id, price)
(假设存在) 或者至少使用 category_id_idx
索引。
Optimizer Trace 分析步骤:
SET optimizer_trace="enabled=on,one_line=off"; SET end_markers_in_json=on;
SELECT * FROM products WHERE category_id = 5 AND price BETWEEN 100 AND 200;
SELECT trace FROM information_schema.optimizer_trace\G
**join_optimization**
** 阶段的子步骤, 特别是****analyzing_range_alternatives
**analyzing_range_alternatives
步骤的示例 (简化版):
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{ //优化器考虑的两个索引
"index": "category_id_idx",
"ranges": ["5 <= category_id <= 5"], // 等价于 category_id=5
"index_dives_for_eq_ranges": true, // 对等值查询进行了索引下潜精确统计
"rowid_ordered": false, // 索引范围扫描结果未按主键排序
"using_mrr": false, // 未使用 MRR 优化
"index_only": false, // 需要回表(查询列未完全覆盖索引)
"rows": 500, // 预估扫描 500 行
"cost": 5000, // 该索引的总成本估算
"chosen": false // 未选择此索引 可能:- 该索引的选择性低(`category_id=5` 实际匹配大量数据)。
},
{
"index": "price_idx",
"ranges": [
"100 <= price <= 200"
],
"index_dives_for_eq_ranges": false, // 范围查询未使用索引下潜
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 2000,
"cost": 8000,
"chosen": false
}
],
"considered_execution_plans": [
{
"plan_prefix": [],
"table": "`products`",
"access_type": "ALL",
"rows": 10000,
"cost": 100000,
"chosen": true
}
]
}
问题诊断:
Optimizer Trace 结果显示, 优化器考虑了category_id_idx
和price_idx
索引, 但最终选择了全表扫描(access_type: ALL
).
chosen: false
表明索引未被选中, 通过查看每个索引的cost
和rows
, 可以发现优化器认为索引扫描成本高于全表扫描.
可能的原因:
JSON 数据可能存在 截断或简化,实际优化器决策中:
索引回表成本未完全体现:示例中的 cost=5000
可能仅包含索引扫描成本,未计算回表成本。
全表扫描顺序 IO 优势:若表数据量较小且完全在内存中(InnoDB Buffer Pool 命中率高),全表扫描可能更高效。
优化方案:
检查索引选择性: 分析 category_id
和 price
列的数据分布, 确认索引选择性.
更新统计信息: ANALYZE TABLE products;
强制使用索引 (谨慎): 如果确定索引有效, 可以尝试 FORCE INDEX
.
Optimizer Trace 的高级用法与最佳实践
结合 EXPLAIN
** 使用:** 先用 EXPLAIN
初步分析, 再用 Optimizer Trace 深入分析。
按需开启: 只在需要诊断时开启, 不要在生产环境长时间开启。
分析关键步骤: 重点关注join_optimization
阶段的子步骤。
结合 Performance Schema 和 Profiling: 获取更全面的性能信息。
学习积累经验: 多分析案例, 逐步掌握.
设置合理的**optimizer_trace_max_mem_size**
: 如果Trace信息过大, 可能会被截断, 可以适当增大该值.
总结
MySQL Optimizer Trace 是查询优化领域的 “核武器”。它提供了前所未有的优化器内部信息, 让我们能够深入了解优化器的决策过程。虽然 Optimizer Trace 的学习曲线较陡峭, 但一旦掌握, 它将成为解决复杂查询性能问题的终极利器。
参考:https://relph1119.github.io/mysql-learning-notes/#/mysql ,推荐理解本文之后去看原书,原书有一定深度需前后贯穿仔细理解