文章目录
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 功能。
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 会产生一定的性能开销,因此 只在需要诊断性能问题时开启,不要在生产环境长时间开启。
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
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:
SET optimizer_trace="enabled=on,one_line=off"; SET end_markers_in_json=on;
- 执行 SQL 查询:
SELECT * FROM products WHERE category_id = 5 AND price BETWEEN 100 AND 200;
- 查看 Optimizer Trace 结果:
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 ,推荐理解本文之后去看原书,原书有一定深度需前后贯穿仔细理解