Bootstrap

mysql之MySQL 优化器追踪 (Optimizer Trace):洞察优化器决策的每一个细节

MySQL 优化器追踪 (Optimizer Trace):洞察优化器决策的每一个细节

EXPLAIN 命令是查询优化的基础工具,但对于复杂的 SQL 查询,EXPLAIN 的输出可能不够详细,难以深入了解优化器的决策过程。 这时,Optimizer Trace (优化器追踪) 就成为了更强大的 “神兵利器”。 Optimizer Trace 能够记录 MySQL 优化器在优化查询过程中 每一步的决策细节,包括成本估算、规则应用、执行计划选择等等,为我们提供了前所未有的 透明度和可控性,是诊断和解决疑难查询性能问题的终极武器。

Optimizer Trace 的核心价值:深入优化器内部,揭秘优化过程

Optimizer Trace 不仅仅是展示最终的执行计划,而是 完整记录了优化器从接收 SQL 查询到最终选择执行计划的整个过程。 它就像是优化器的 “debug 日志”,将优化器的思考过程 “原原本本” 地呈现出来,让我们能够:

  • 理解优化器的决策过程: 清晰地看到优化器是如何生成候选执行计划、如何进行成本估算、如何应用优化规则、以及最终选择了哪个执行计划。

  • 定位优化瓶颈: 精确地找出优化过程中哪个环节出现了问题,例如,成本估算不准确、规则应用不合理、索引选择错误等等。

  • 验证优化策略: 检验我们的优化思路是否有效,例如,添加索引是否真的被优化器使用,查询改写是否真的提升了性能。

  • 深入学习优化器原理: 通过分析 Optimizer Trace 的输出,可以更深入地理解 MySQL 优化器的内部工作机制,提升自身的优化技能。

Optimizer Trace 的基本用法

Optimizer Trace 的使用步骤主要分为三个阶段:

  1. 开启 Optimizer Trace: 通过设置 optimizer_traceend_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 结果。

  1. 执行需要追踪的 SQL 查询: 正常执行需要分析的 SQL 查询语句。 注意: 开启 Optimizer Trace 会产生一定的性能开销,因此 只在需要诊断性能问题时开启,不要在生产环境长时间开启。
SELECT * FROM orders WHERE order_date >= '2023-01-01' AND customer_id = 123 ORDER BY order_id DESC LIMIT 10;
  1. 查看 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 分析步骤:

  1. 开启 Optimizer Trace:
SET optimizer_trace="enabled=on,one_line=off"; SET end_markers_in_json=on;
  1. 执行 SQL 查询:
SELECT * FROM products WHERE category_id = 5 AND price BETWEEN 100 AND 200;
  1. 查看 Optimizer Trace 结果:
SELECT trace FROM information_schema.optimizer_trace\G
  1. 分析**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_idxprice_idx索引, 但最终选择了全表扫描(access_type: ALL).

  • chosen: false 表明索引未被选中, 通过查看每个索引的costrows, 可以发现优化器认为索引扫描成本高于全表扫描.

可能的原因:

JSON 数据可能存在 截断或简化,实际优化器决策中:

  • 索引回表成本未完全体现:示例中的 cost=5000 可能仅包含索引扫描成本,未计算回表成本。

  • 全表扫描顺序 IO 优势:若表数据量较小且完全在内存中(InnoDB Buffer Pool 命中率高),全表扫描可能更高效。

优化方案:

  1. 检查索引选择性: 分析 category_idprice 列的数据分布, 确认索引选择性.

  2. 更新统计信息: ANALYZE TABLE products;

  3. 强制使用索引 (谨慎): 如果确定索引有效, 可以尝试 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 ,推荐理解本文之后去看原书,原书有一定深度需前后贯穿仔细理解

;