【数据库】【Mysql】慢SQL深度分析:EXPLAIN 与 optimizer_trace 全解析

MySQL 慢SQL深度分析:EXPLAIN 与 optimizer_trace 全解析

在 MySQL 性能优化体系中,慢SQL分析是核心技能。本文将从 EXPLAIN 执行计划解读optimizer_trace 优化器追踪,构建完整的慢查询诊断方法论。


一、EXPLAIN 基础与核心字段总览

1.1 EXPLAIN 使用方法

基础语法

sql 复制代码
-- 分析 SELECT 语句
EXPLAIN SELECT * FROM orders WHERE user_id = 100;

-- 获取 JSON 格式详细计划(推荐用于复杂分析)
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 100;

-- 分析 UPDATE/DELETE(MySQL 5.6+)
EXPLAIN UPDATE orders SET status = 'paid' WHERE user_id = 100;

关键输出字段

sql 复制代码
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key      | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+

分析优先级type → key → rows → Extra,这四个字段决定了查询性能基线。


二、type 字段详解:访问类型优化路径

type 字段表示表连接类型,是判断查询效率的核心指标,性能从好到差排序如下:

2.1 最优类型:system 与 const

system :表中仅有一条数据,是 const 类型的特例

sql 复制代码
-- 示例:系统配置表仅一行
EXPLAIN SELECT * FROM config WHERE id = 1;

const:使用主键或唯一索引进行等值匹配,最多返回一条记录

sql 复制代码
-- 示例:主键等值查询
EXPLAIN SELECT * FROM users WHERE id = 100; -- type: const

性能特征 :这些类型代表单表访问 ,性能最优,查询时间为常量级

2.2 高效类型:eq_ref 与 ref

eq_ref :连接查询中,驱动表通过唯一索引访问被驱动表

sql 复制代码
-- 示例:employees 表通过主键关联 salaries 表
EXPLAIN 
SELECT e.*, s.salary 
FROM employees e 
JOIN salaries s ON e.emp_no = s.emp_no; -- s 表 type: eq_ref

ref :使用非唯一索引进行等值匹配,可能返回多条记录

sql 复制代码
-- 示例:通过普通索引查询
EXPLAIN SELECT * FROM orders WHERE user_id = 100; -- type: ref

优化目标核心业务 SQL 至少达到 ref 级别

2.3 中等类型:range 与 index

range :索引范围扫描,常用于 >, <, BETWEEN, IN 等操作

sql 复制代码
-- 示例:时间范围查询
EXPLAIN SELECT * FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31';

index:全索引扫描,遍历整个索引树,比全表扫描快

sql 复制代码
-- 示例:查询索引列(无需回表)
EXPLAIN SELECT user_id FROM orders; -- 只查索引列

注意index 虽然比 ALL 快,但对大表仍是性能隐患。

2.4 最差类型:ALL(全表扫描)

ALL :全表扫描,必须优化

sql 复制代码
-- 示例:无索引或索引失效
EXPLAIN SELECT * FROM orders WHERE YEAR(create_time) = 2024; -- type: ALL
EXPLAIN SELECT * FROM orders WHERE status = 'shipped'; -- status 无索引

优化路径

  1. 添加索引:为 WHERE 条件列创建索引
  2. 避免函数YEAR(create_time)create_time >= '2024-01-01'
  3. 强制索引FORCE INDEX(idx_create_time)

2.5 type 优化检查清单

type 类型 性能等级 是否需要优化 典型场景
system ★★★★★ 无需 单行配置表
const ★★★★★ 无需 主键等值查询
eq_ref ★★★★★ 无需 JOIN 主键关联
ref ★★★★☆ 可接受 普通索引等值查询
range ★★★☆☆ 观察 小范围查询
index ★★☆☆☆ 建议优化 全索引扫描
ALL ★☆☆☆☆ 必须优化 全表扫描

三、ref、rows、filtered 字段深度解读

3.1 ref:索引引用列

含义:显示哪些列或常量被用于索引查找

sql 复制代码
-- 示例:常量引用
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- ref: const

-- 示例:多表连接
EXPLAIN 
SELECT o.*, u.name 
FROM orders o 
JOIN users u ON o.user_id = u.id;
-- orders 表 ref: const (若 user_id = 100)
-- users 表 ref: db.o.user_id (引用 orders 表的 user_id)

3.2 rows:预估扫描行数

核心作用 :MySQL 优化器预估需要读取的行数。值越小越好,但需注意:

  • 预估值:基于统计信息,可能不准确
  • 优化目标rows < 1000 为佳,rows > 10000 需警惕

统计信息更新

sql 复制代码
-- 当 rows 预估偏差大时,更新统计信息
ANALYZE TABLE orders;

实战案例

sql 复制代码
-- 慢查询:rows=980000
SELECT * FROM orders WHERE user_id=1000 AND status='shipped' ORDER BY create_time DESC LIMIT 10;

-- EXPLAIN 分析:rows 过大,说明索引选择性差
-- 优化:创建 (user_id, status, create_time) 复合索引

3.3 filtered:条件过滤率

含义:表示返回结果的行占扫描行数的百分比

sql 复制代码
-- 示例:扫描100行,返回10行,filtered=10%
EXPLAIN SELECT * FROM users WHERE age > 25 AND city = 'Beijing';

优化意义filtered 值低说明索引选择性差,需考虑:

  1. 更换索引列:将高选择性列放前面
  2. 覆盖索引:避免回表

四、Extra 字段:性能优化的"宝藏提示"

Extra 字段包含查询执行的额外信息,是性能诊断的关键线索。

4.1 高危信号:必须优化

Using filesort:无法使用索引排序,需额外排序操作

sql 复制代码
-- 触发场景:ORDER BY 非索引列或索引列顺序错误
EXPLAIN SELECT * FROM orders ORDER BY amount; -- amount 无索引

-- 优化:创建排序索引
ALTER TABLE orders ADD INDEX idx_create_time (create_time);
EXPLAIN SELECT * FROM orders ORDER BY create_time; -- Extra: NULL

Using temporary:需创建临时表存储中间结果

sql 复制代码
-- 触发场景:DISTINCT、GROUP BY、ORDER BY 无索引
EXPLAIN SELECT DISTINCT user_id FROM orders; -- 无索引
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status; -- status 无索引

-- 优化:添加索引
ALTER TABLE orders ADD INDEX idx_status (status);

4.2 优化提示:建议改进

Using index :使用覆盖索引,无需回表

sql 复制代码
-- 理想状态
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 100;
-- Extra: Using index (覆盖索引)

Using index condition:使用索引下推(ICP)

sql 复制代码
-- 复合索引 (name, age)
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age = 10;
-- Extra: Using index condition (age 条件在存储引擎层过滤)

Using MRR:使用多范围读取优化

sql 复制代码
-- 范围查询
EXPLAIN SELECT * FROM orders WHERE user_id BETWEEN 100 AND 200;
-- Extra: Using MRR (回表时顺序读取)

4.3 Extra 字段优化口诀

Extra 值 含义 优化动作
Using filesort 文件排序 添加排序索引
Using temporary 临时表 添加 GROUP BY/ORDER BY 索引
Using index 覆盖索引 保持现状,理想状态
Using index condition 索引下推 确认 ICP 开启
Using MRR 多范围读取 确认 MRR 开启
Using where Server 层过滤 考虑索引覆盖
NULL 正常 无需优化

五、optimizer_trace:优化器决策黑盒破解

5.1 什么是 optimizer_trace?

optimizer_trace 是 MySQL 5.6 引入的优化器决策追踪工具,记录优化器如何选择执行计划的完整过程。它解决了 EXPLAIN 的局限性:

  • EXPLAIN 只展示最终选择的执行计划
  • optimizer_trace 展示为什么选这个计划(包括所有备选方案和成本计算)

5.2 开启与配置

会话级开启(推荐):

sql 复制代码
-- 开启 trace
SET optimizer_trace="enabled=on",end_markers_in_json=on;

-- 执行需要分析的 SQL
SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';

-- 查看 trace 结果
SELECT * FROM information_schema.optimizer_trace \G;

全局开启(慎用):

sql 复制代码
SET GLOBAL optimizer_trace="enabled=on",end_markers_in_json=on;

核心参数

sql 复制代码
-- 控制 trace 内存大小(默认 1MB)
SET optimizer_trace_max_mem_size = 1048576;

-- 限制返回的 trace 条数(默认 1)
SET optimizer_trace_limit = 1;

-- 控制跟踪内容(默认全部开启)
SET optimizer_trace_features = 'greedy_search=on,range_optimizer=on,dynamic_range=on';

5.3 trace 结构解析

trace 以 JSON 格式存储,包含 3 大阶段:

阶段 1:join_preparation(SQL 准备)
json 复制代码
{
  "join_preparation": {
    "select#": 1,
    "steps": [
      {
        "expanded_query": "/* select#1 */ select `orders`.`id` AS `id` ... from `orders` where ..."
      }
    ]
  }
}

作用:展示 SQL 重写后的标准格式,确认优化器接收到的查询。

阶段 2:join_optimization(核心优化阶段)

子阶段 2.1:condition_processing(条件处理)

json 复制代码
{
  "condition_processing": {
    "condition": "WHERE",
    "original_condition": "((`orders`.`user_id` = 100) AND (`orders`.`status` = 'shipped'))",
    "steps": [
      {
        "transformation": "equality_propagation",
        "resulting_condition": "(multiple equal(100, `orders`.`user_id`) ...)"
      }
    ]
  }
}

作用:展示条件重写过程(等值传播、常量传播)。

子阶段 2.2:rows_estimation(行数估算)

json 复制代码
{
  "rows_estimation": [
    {
      "table": "`orders`",
      "range_analysis": {
        "table_scan": {
          "rows": 1000000,
          "cost": 101000
        },
        "potential_range_indexes": [
          {
            "index": "idx_user_id",
            "usable": true,
            "key_parts": ["user_id"]
          }
        ]
      }
    }
  ]
}

作用 :对比全表扫描 vs 索引扫描的成本,揭示为什么选择某个索引

子阶段 2.3:considered_execution_plans(执行计划选择)

json 复制代码
{
  "considered_execution_plans": [
    {
      "plan_prefix": [],
      "table": "`orders`",
      "best_access_path": {
        "considered_access_paths": [
          {
            "access_type": "ref",
            "index": "idx_user_id",
            "rows": 100,
            "cost": 120.5,
            "chosen": true
          },
          {
            "access_type": "ALL",
            "rows": 1000000,
            "cost": 101000,
            "chosen": false
          }
        ]
      },
      "cost_for_plan": 120.5,
      "rows_for_plan": 100,
      "chosen": true
    }
  ]
}

作用 :展示所有候选计划的成本对比,最终选择 cost 最小的计划

阶段 3:join_execution(执行阶段)
json 复制代码
{
  "join_execution": {
    "select#": 1,
    "steps": []
  }
}

5.4 实战案例:索引选择之谜

问题:为什么有时 MySQL 不选"最优"索引?

场景 :表 orders 有索引 idx_user_ididx_status,查询:

sql 复制代码
SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';

EXPLAIN 结果

sql 复制代码
-- 可能使用了 idx_status,而非 idx_user_id
type: ref
key: idx_status
rows: 5000

optimizer_trace 分析

sql 复制代码
-- 开启 trace
SET optimizer_trace="enabled=on";

-- 执行查询
SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';

-- 查看 trace
SELECT * FROM information_schema.optimizer_trace \G;

trace 关键信息

json 复制代码
{
  "potential_range_indexes": [
    {
      "index": "idx_user_id",
      "rows": 100,          // user_id=100 过滤后约 100 行
      "cost": 150.0
    },
    {
      "index": "idx_status",
      "rows": 5000,         // status='shipped' 过滤后约 5000 行
      "cost": 120.0         // 但 cost 更低!
    }
  ]
}

真相 :优化器认为 idx_statuscost 更低 (可能因为 idx_user_id 的回表成本高),但实际 idx_user_id 更优。

解决方案

sql 复制代码
-- 方案1:强制使用索引
SELECT * FROM orders FORCE INDEX(idx_user_id) WHERE user_id = 100 AND status = 'shipped';

-- 方案2:创建复合索引
ALTER TABLE orders ADD INDEX idx_user_status(user_id, status);

六、慢SQL分析完整流程

6.1 四步诊断法

Step 1:识别慢SQL

sql 复制代码
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 阈值1秒

-- 查看慢SQL日志
mysqldumpslow -s t /var/log/mysql/slow.log | head -20;

Step 2:EXPLAIN 初步分析

sql 复制代码
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'shipped';
-- 重点关注:type, key, rows, Extra

Step 3:optimizer_trace 深度追踪(当 EXPLAIN 无法解释时)

sql 复制代码
SET optimizer_trace="enabled=on";
-- 执行SQL
-- 查看 trace,分析优化器决策

Step 4:针对性优化

  • 索引优化:添加/修改索引
  • SQL重写:避免函数、转换条件
  • 结构优化:分表分区
  • 参数调优 :调整 join_buffer_size, read_rnd_buffer_size

6.2 实战案例:从慢查询到优化

原始慢SQL

sql 复制代码
-- 执行时间:5秒
SELECT * FROM orders 
WHERE DATE(create_time) = '2024-01-01' 
ORDER BY id 
LIMIT 10;

Step 1:EXPLAIN 分析

sql 复制代码
EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01' ORDER BY id LIMIT 10;

-- 结果:
-- type: ALL(全表扫描)
-- key: NULL(未使用索引)
-- rows: 1000000
-- Extra: Using where

问题定位

  • DATE(create_time) 导致索引失效
  • ORDER BY id 产生 filesort

Step 2:optimizer_trace 验证

sql 复制代码
-- 开启 trace
SET optimizer_trace="enabled=on";

-- trace 显示:优化器放弃 idx_create_time 索引,因为函数操作
"condition_processing": {
  "transformation": "func_date_conversion",
  "index_unusable": true
}

Step 3:优化方案

sql 复制代码
-- 优化后SQL(执行时间:50ms)
SELECT * FROM orders 
WHERE create_time >= '2024-01-01 00:00:00' 
  AND create_time < '2024-01-02 00:00:00' 
ORDER BY id 
LIMIT 10;

-- 添加复合索引
ALTER TABLE orders ADD INDEX idx_ctime_id(create_time, id);

Step 4:验证优化效果

sql 复制代码
EXPLAIN SELECT * FROM orders 
WHERE create_time >= '2024-01-01 00:00:00' 
  AND create_time < '2024-01-02 00:00:00' 
ORDER BY id 
LIMIT 10;

-- 结果:
-- type: range
-- key: idx_ctime_id
-- rows: 100
-- Extra: Using index condition
-- 性能提升:5秒 → 50ms(100倍)

七、高级技巧与避坑指南

7.1 optimizer_trace 实战技巧

场景1:多个索引的选择困惑

sql 复制代码
-- 表有 idx_a, idx_b, idx_c,查询 WHERE a=1 AND b=2 AND c=3
-- EXPLAIN 显示使用了 idx_c,但不确定为何不用 idx_ab

-- 使用 trace 查看 cost 计算
"considered_execution_plans": [
  {"access_type": "ref", "index": "idx_ab", "cost": 200, "chosen": false},
  {"access_type": "ref", "index": "idx_c", "cost": 150, "chosen": true}
]

场景2:JOIN 顺序优化

sql 复制代码
-- 三表 JOIN,优化器选择了错误的驱动表
-- trace 中查看 table_dependencies 和 considered_plans

7.2 常见误区

误区1:rows 越小越好

  • 真相 :rows 是预估值,filtered 列同样重要。可能扫描100行过滤出10行(filtered=10%),不一定比扫描500行过滤出400行(filtered=80%)更优。

误区2:有索引就一定会用

  • 真相 :优化器基于 cost 决策,可能因回表成本高数据分布不均 等原因放弃索引。此时需用 FORCE INDEX 或优化索引设计。

误区3:EXPLAIN 显示 Using index 就完美

  • 真相 :需确认是否 Using filesortUsing temporary,覆盖索引不能解决所有问题。

7.3 慢SQL优化口诀

复制代码
索引失效三宗罪:函数计算、隐式转换、左模糊
排序优化两法宝:索引覆盖、最左前缀
连接查询要记牢:小表驱动、索引关联
trace 分析三步走:开启执行、查看 JSON、对比 cost

八、总结:从表象到本质的优化思维

工具 作用 适用场景 输出价值
EXPLAIN 执行计划快照 日常快速诊断 type/key/rows/Extra
optimizer_trace 优化器决策过程 索引选择异常、复杂查询 成本计算、备选方案
慢查询日志 性能监控 定位慢SQL 执行时间、扫描行数

核心方法论

  1. 先监控:通过慢日志定位慢SQL
  2. 再快照:用 EXPLAIN 获取执行计划
  3. 后追踪:用 optimizer_trace 破解决策黑盒
  4. 终验证:优化后重复分析,形成闭环

真正的性能优化不是"调参数",而是理解 MySQL 优化器的决策逻辑,让 SQL、索引、数据分布三者协同工作。掌握 EXPLAIN 和 optimizer_trace,就相当于拥有了慢SQL分析的"CT扫描仪"和"病理报告"。

相关推荐
XerCis2 小时前
PostgreSQL超全指南
数据库·postgresql
数据知道2 小时前
万字详解模式(Schema):如何利用 Schema 实现PostgreSQL中开发/测试/生产环境隔离
数据库·postgresql
WilliamHu.2 小时前
智能体项目实战
数据库·oracle
数据知道2 小时前
PostgreSQL实战:详细讲述UUID主键,以及如何生成无热点的分布式主键
数据库·分布式·postgresql
数据知道2 小时前
PostgreSQL实战:如何选择合适的数据类型?
数据库·postgresql
nvd112 小时前
Pytest 异步数据库测试实战:基于 AsyncMock 的无副作用打桩方案
数据库·pytest
heze093 小时前
sqli-labs-Less-16自动化注入方法
mysql·网络安全·自动化
os_lee3 小时前
Milvus 实战教程(Go 版本 + Ollama bge-m3 向量模型)
数据库·golang·milvus
laplace01233 小时前
向量库 Qdrant + 图数据库Neo4j+Embedding阿里百炼text-embedding-v3
数据库·embedding·agent·neo4j