MySQL 优化器追踪 (Optimizer Trace):洞察优化器决策的每一个细节
EXPLAIN 命令是查询优化的基础工具,但对于复杂的 SQL 查询,EXPLAIN 的输出可能不够详细,难以深入了解优化器的决策过程。 这时,Optimizer Trace (优化器追踪) 就成为了更强大的 “神兵利器”。 Optimizer Trace 能够记录 MySQL 优化器在优化查询过程中 每一步的决策细节,包括成本估算、规则应用、执行计划选择等等,为我们提供了前所未有的 透明度和可控性,是诊断和解决疑难查询性能问题的终极武器。
Optimizer Trace 的核心价值:深入优化器内部,揭秘优化过程
Optimizer Trace 不仅仅是展示最终的执行计划,而是 完整记录了优化器从接收 SQL 查询到最终选择执行计划的整个过程。 它就像是优化器的 “debug 日志”,将优化器的思考过程 “原原本本” 地呈现出来,让我们能够:
理解优化器的决策过程: 清晰地看到优化器是如何生成候选执行计划、如何进行成本估算、如何应用优化规则、以及最终选择了哪个执行计划。
定位优化瓶颈: 精确地找出优化过程中哪个环节出现了问题,例如,成本估算不准确、规则应用不合理、索引选择错误等等。
验证优化策略: 检验我们的优化思路是否有效,例如,添加索引是否真的被优化器使用,查询改写是否真的提升了性能。
深入学习优化器原理: 通过分析 Optimizer Trace 的输出,可以更深入地理解 MySQL 优化器的内部工作机制,提升自身的优化技能。
Optimizer Trace 的基本用法
Optimizer Trace 的使用步骤主要分为三个阶段:
- 开启 Optimizer Trace: 通过设置
optimizer_trace和end_markers_in_json系统变量来开启 Optimizer Trace 功能。
sql 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 结果。
- 执行需要追踪的 SQL 查询: 正常执行需要分析的 SQL 查询语句。 注意: 开启 Optimizer Trace 会产生一定的性能开销,因此 只在需要诊断性能问题时开启,不要在生产环境长时间开启。
sql SELECT * FROM orders WHERE order_date >= '2023-01-01' AND customer_id = 123 ORDER BY order_id DESC LIMIT 10;
- 查看 Optimizer Trace 结果: 执行完 SQL 查询后,可以通过查询
information_schema.OPTIMIZER_TRACE表 或performance_schema.optimizer_trace表来获取 Optimizer Trace 的详细信息。 MySQL 8及以上版本推荐information_schema.OPTIMIZER_TRACE
sql 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 结构详解)
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 分析步骤:
- 开启 Optimizer Trace:
sql SET optimizer_trace="enabled=on,one_line=off"; SET end_markers_in_json=on;
- 执行 SQL 查询:
sql SELECT * FROM products WHERE category_id = 5 AND price BETWEEN 100 AND 200;
- 查看 Optimizer Trace 结果:
sql 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 的学习曲线较陡峭, 但一旦掌握, 它将成为解决复杂查询性能问题的终极利器。
