ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL 8.0树状执行计划优化SQL性能实战

MySQL 8.0树状执行计划优化SQL性能实战

1. MySQL 执行计划可视化革命

记得第一次在MySQL 5.7时代分析复杂查询时,面对密密麻麻的EXPLAIN输出表格,我不得不打印出十几页纸,用荧光笔标记各个表的连接顺序,再铺满整个办公桌才能理清执行路径。这种"平面化"的执行计划展示方式,对于多表关联查询简直就是灾难——直到MySQL 8.0带来了FORMAT=TREE这个改变游戏规则的参数。

在MySQL 8.0.16版本中,官方引入了EXPLAIN FORMAT=TREE语法,它彻底改变了DBA和开发者阅读执行计划的方式。不同于传统表格形式的执行计划输出,树状结构天然符合SQL语句的实际执行流程——从最内层的子查询开始,逐步向外层推进,最终完成整个查询。这种展示方式让执行计划的阅读体验从"解码密码"变成了"看图说话"。

2. 树状执行计划实战演示

2.1 基础使用姿势

要让执行计划"站起来",只需在常规EXPLAIN语句后追加FORMAT=TREE参数:

EXPLAIN FORMAT=TREE SELECT e.emp_no, d.dept_name FROM employees e JOIN dept_emp de ON e.emp_no = de.emp_no JOIN departments d ON de.dept_no = d.dept_no WHERE e.hire_date > '1990-01-01';

执行后你会看到类似这样的树状输出:

-> Nested loop inner join -> Nested loop inner join -> Index scan on e using idx_hire_date -> Filter: (e.hire_date > DATE'1990-01-01') -> Index lookup on de using PRIMARY (emp_no=e.emp_no) -> Single-row index lookup on d using PRIMARY (dept_no=de.dept_no)

这个输出清晰地展示了:

  • 查询从最内层的employees表过滤开始(Filter)
  • 通过emp_no关联到dept_emp表(第一个Nested loop)
  • 最后关联到departments表获取部门名称(第二个Nested loop)

2.2 与传统表格形式的对比

我们通过同一查询的两种EXPLAIN输出来感受差异:

传统表格形式(EXPLAIN FORMAT=TRADITIONAL):

+----+-------------+-------+------------+-------+---------------+---------+---------+-------------------+--------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+---------+---------+-------------------+--------+----------+-------------+ | 1 | SIMPLE | e | NULL | range | idx_hire_date | idx_hire_date | 3 | NULL | 1000 | 100.00 | Using where | | 1 | SIMPLE | de | NULL | ref | PRIMARY | PRIMARY | 4 | employees.e.emp_no | 1 | 100.00 | NULL | | 1 | SIMPLE | d | NULL | eq_ref| PRIMARY | PRIMARY | 12 | de.dept_no | 1 | 100.00 | NULL | +----+-------------+-------+------------+-------+---------------+---------+---------+-------------------+--------+----------+-------------+

树状形式(EXPLAIN FORMAT=TREE):

-> Nested loop inner join -> Nested loop inner join -> Index scan on e using idx_hire_date -> Filter: (e.hire_date > DATE'1990-01-01') -> Index lookup on de using PRIMARY (emp_no=e.emp_no) -> Single-row index lookup on d using PRIMARY (dept_no=de.dept_no)

表格形式需要你通过id和select_type字段脑补执行顺序,而树状形式直接展示了完整的执行路径。特别是在分析包含子查询、UNION等复杂操作时,树状结构的优势更加明显。

3. 进阶技巧与实战应用

3.1 结合ANALYZE获取实时执行数据

MySQL 8.0.18进一步强化了这个功能,引入了EXPLAIN ANALYZE语法。它会实际执行查询并返回真实的执行统计信息:

EXPLAIN ANALYZE FORMAT=TREE SELECT /*+ MAX_EXECUTION_TIME(1000) */ e.emp_no, CONCAT(e.first_name, ' ', e.last_name) AS name FROM employees e WHERE e.hire_date BETWEEN '1985-01-01' AND '1995-01-01' ORDER BY e.hire_date DESC LIMIT 100;

输出示例:

-> Limit: 100 row(s) -> Sort: e.hire_date DESC, limit input to 100 row(s) per chunk -> Filter: (e.hire_date between DATE'1985-01-01' and DATE'1995-01-01') -> Index scan on e using idx_hire_date (actual time=0.5..15.2 rows=10000 loops=1)

关键改进点:

  • actual time=0.5..15.2显示实际执行时间(毫秒)
  • rows=10000显示实际扫描行数
  • loops=1显示迭代次数

这些实时数据对于精准定位性能瓶颈至关重要。我曾用这个功能发现过一个看似高效的索引扫描实际需要读取的行数是优化器预估的3倍,最终通过创建覆盖索引解决了问题。

3.2 可视化工具集成

虽然终端输出的树状结构已经足够清晰,但我们可以通过一些技巧获得更好的可视化效果:

  1. 复制到文本编辑器:树状文本复制到VS Code等支持缩进高亮的编辑器,配合Monospace字体更易读
  2. 使用MySQL Workbench:8.0+版本的可视化执行计划功能会自动解析TREE格式
  3. 转成图形化工具:通过以下命令将结果导出为JSON,再导入第三方可视化工具:
EXPLAIN FORMAT=JSON SELECT ... -- 你的查询语句

4. 性能优化实战案例

4.1 识别隐式类型转换

通过树状执行计划可以快速发现隐式类型转换问题。例如:

EXPLAIN FORMAT=TREE SELECT * FROM orders WHERE user_id = '10086'; -- user_id是INT类型

输出中的警告信息会明确提示:

-> Filter: (orders.user_id = 10086) (warning: Implicit conversion from string to int)

这个提示在传统表格形式中很容易被忽略,但在树状结构中非常醒目。我曾通过这个提示发现过前端传参类型错误导致的性能问题,修正后查询速度提升了20倍。

4.2 优化复杂子查询

考虑这个包含派生表的查询:

EXPLAIN FORMAT=TREE SELECT d.dept_name, t.avg_salary FROM departments d JOIN ( SELECT de.dept_no, AVG(s.salary) AS avg_salary FROM dept_emp de JOIN salaries s ON de.emp_no = s.emp_no WHERE s.to_date > NOW() GROUP BY de.dept_no ) t ON d.dept_no = t.dept_no;

树状输出会清晰展示临时表的创建和填充过程:

-> Nested loop inner join -> Table scan on d -> Index lookup on t using <auto_key0> (dept_no=d.dept_no) -> Materialize -> Group aggregate: avg(s.salary) -> Nested loop inner join -> Index scan on de using PRIMARY -> Filter: (s.to_date > NOW()) -> Index lookup on s using PRIMARY (emp_no=de.emp_no)

从这个执行计划可以直观看出:

  1. 系统需要物化(Materialize)派生表t
  2. 物化过程涉及全表扫描dept_emp表
  3. 对每个dept_emp记录都要查询salaries表

基于这些观察,我们可以考虑:

  • 为dept_emp表的dept_no字段添加索引
  • 将子查询改为JOIN方式
  • 使用CTE(Common Table Expression)提高可读性

5. 注意事项与最佳实践

5.1 版本兼容性考虑

虽然FORMAT=TREE非常实用,但需要注意:

  • 仅MySQL 8.0.16+支持基础功能
  • EXPLAIN ANALYZE需要8.0.18+
  • 某些特性如EXPLAIN FORMAT=TREE WITH ANALYZE需要8.0.21+

在生产环境使用时,务必确认MySQL版本。我有次在升级前测试新特性时,就遇到过开发环境使用8.0.21而生产环境还是8.0.15的尴尬情况。

5.2 权限与性能影响

使用EXPLAIN ANALYZE时要注意:

  • 需要实际执行查询,可能对生产环境造成影响
  • 大型查询可能消耗大量资源
  • 需要足够的权限(包括SHOW VIEW和PROCESS)

建议的做法:

  • 在测试环境先使用普通EXPLAIN
  • 使用MAX_EXECUTION_TIME提示限制执行时间
  • 对大数据量查询使用LIMIT子句

5.3 与其他工具配合

树状执行计划可以与其他优化工具完美配合:

  1. 性能模式(Performance Schema):结合EXPLAIN ANALYZE的结果,在Performance Schema中查找对应的等待事件
  2. sys Schema:通过sys.session视图查看正在执行的查询计划
  3. 优化器提示:使用/*+ INDEX(...) */等提示后,在树状计划中验证是否生效

我在实际工作中经常使用这样的组合拳:

-- 第一步:获取基础执行计划 EXPLAIN FORMAT=TREE SELECT ...; -- 第二步:添加优化器提示后验证 EXPLAIN FORMAT=TREE SELECT /*+ INDEX(tbl idx_col) */ ...; -- 第三步:在测试环境分析实际执行 EXPLAIN ANALYZE FORMAT=TREE SELECT ...; -- 第四步:在生产环境通过Performance Schema监控 SELECT * FROM performance_schema.events_statements_history_long WHERE SQL_TEXT LIKE '%关键部分%';

这种从静态分析到动态监控的完整链路,能有效解决90%以上的SQL性能问题。树状执行计划在这个流程中扮演着"可视化地图"的关键角色,让整个优化过程更加直观高效。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进