如何用 EXPLAIN ANALYZE / SHOW WARNINGS 看真实执行成本?
How to use EXPLAIN ANALYZE / SHOW WARNINGS to view actual execution cost
考察要点
这道题面试官想看你是否具备实际的 SQL 性能诊断能力,而不只是停留在理论层面。它考察你是否理解并能熟练运用 MySQL 提供的两大诊断利器:
EXPLAIN ANALYZE:评估 SQL 真实执行成本与预估执行计划之间差异的能力。SHOW WARNINGS:洞察 MySQL 查询优化器重写行为、发现隐式转换等导致索引失效根源的能力。
掌握这两者,是区分“知道”EXPLAIN和“精通”EXPLAIN的关键分水岭。
核心概念
标准的 EXPLAIN 只显示优化器预估的执行计划,其成本和行数(rows)是基于统计信息的估算,可能与真实情况有很大偏差。EXPLAIN ANALYZE 和 SHOW WARNINGS 则是深入真实执行细节的工具。
1. EXPLAIN ANALYZE:获取真实执行指标
EXPLAIN ANALYZE 在 MySQL 8.0.18 版本中引入,它会实际执行查询,然后展示带有真实成本和行数的执行计划树。
核心价值:对比“预估”与“现实”,精准定位性能瓶颈。
输出格式与关键指标解读: 它采用“火山模型”或“迭代器模型”的树状格式展示。
1-- 示例表2CREATE TABLE employees (3 id INT PRIMARY KEY AUTO_INCREMENT,4 name VARCHAR(100),5 department_id INT,6 salary INT,7 INDEX idx_dept_salary (department_id, salary)8);9-- 插入一些数据...1011-- 执行分析12EXPLAIN ANALYZE SELECT name, salary FROM employees WHERE department_id = 10 AND salary > 80000;
输出示例:
1-> Index range scan on employees using idx_dept_salary (cost=... rows=...) (actual time=0.042..0.085 rows=15 loops=1)2 -> Filter: (employees.salary > 80000) (cost=... rows=...)
关键指标解读:
actual time=0.042..0.085: 这是最重要的指标。0.042(first row time): 该迭代器节点返回第一行数据所花费的时间(毫秒)。0.085(last row time): 该迭代器节点返回所有行数据所花费的总时间(毫秒)。- 如果
first row时间很长,说明启动成本高;如果first row和last row时间差距大,说明数据传输或处理耗时。
rows=15: 该迭代器节点实际返回的行数。这是与EXPLAIN中预估rows最核心的区别。如果预估值和实际值差距巨大,说明统计信息不准,可能导致优化器选择错误的执行计划。loops=1: 该迭代器节点被其父节点执行的次数。在嵌套循环连接(Nested Loop Join)中,内层表的loops数会很高,等于外层表返回的行数。
| 工具 | MySQL 版本 | 是否执行 Query | 成本/行数 | 使用场景 |
|---|---|---|---|---|
EXPLAIN | 所有版本 | 否 | 预估值 | 日常快速检查索引使用情况、连接类型等。 |
EXPLAIN ANALYZE | ✅ 8.0.18+ | 是 | 真实值 | 预估与现实偏差大、需要精准定位耗时算子时。 |
2. SHOW WARNINGS:揭示优化器重写细节
在 EXPLAIN 执行后,MySQL 会在会话中记录一些额外信息,通过 SHOW WARNINGS 可以查看。其中最重要的信息是优化器重写后的最终 SQL。
核心价值:发现因“隐式转换”导致的索引失效。
数据示例:
假设 employees 表中的 name 字段是 VARCHAR 类型。
1-- DDL2CREATE TABLE employees (3 id INT PRIMARY KEY,4 name VARCHAR(100),5 INDEX idx_name (name)6);7INSERT INTO employees VALUES (1, '10086');89-- 错误的查询(用数字去匹配字符串列)10EXPLAIN SELECT * FROM employees WHERE name = 10086;
EXPLAIN 的结果可能显示走了全表扫描(type: ALL),但没有直接告诉你为什么。
使用 SHOW WARNINGS 揭示真相:
1-- 紧跟在 EXPLAIN 之后执行2SHOW WARNINGS;
输出示例:
| Level | Code | Message |
|---|---|---|
| Note | 1003 | /* select#1 */ select test.employees.id AS id,test.employees.name AS name from test.employees where (cast(test.employees.name as unsigned) = 10086) |
关键信息解读:
Message 字段展示了重写后的 SQL:WHERE (cast(employees.name as unsigned) = 10086)。
MySQL 为了比较 VARCHAR 类型的 name 列和数字 10086,对 name 列应用了 CAST 函数。对索引列使用函数,是导致索引失效的典型场景。SHOW WARNINGS 明确地证实了这一点。
正确做法:保证查询参数与列类型一致。
1EXPLAIN SELECT * FROM employees WHERE name = '10086'; -- 使用字符串
此时 EXPLAIN 会显示 type: ref,正确用上了 idx_name 索引。
常见低分回答
- 混淆
EXPLAIN和EXPLAIN ANALYZE:错误地声称EXPLAIN显示的是真实执行时间或行数。 - 不知
EXPLAIN ANALYZE的版本限制:在面试中提及该命令但不知道它在 MySQL 8.0 才可用,会显得知识陈旧。 - 对
EXPLAIN ANALYZE的成本一无所知:认为EXPLAIN ANALYZE和EXPLAIN一样是低成本操作。错误!它会真实执行查询,对于慢查询或UPDATE/DELETE语句,在生产环境中使用是高风险行为。 - 只知索引失效规则,不知如何验证:能背出“列上用函数索引会失效”,但当被问及“如何证明是函数导致的?”时,无法回答出使用
SHOW WARNINGS查看重写后的 SQL 这一关键步骤。 - 误读
actual time=X..Y:将其理解为一个时间范围或不确定值,而不是“首行返回时间..全行返回时间”的精确含义。
工程实践与优化
- 诊断流程:
- 使用
EXPLAIN初步判断执行计划,关注type,key,rows,Extra。 - 如果
type不佳(如ALL,index)或rows预估值与业务经验严重不符,怀疑计划有误。 - 在开发/测试环境,使用
EXPLAIN ANALYZE运行查询。 - 对比
EXPLAIN的rows和ANALYZE的rows,若差异巨大,考虑运行ANALYZE TABLE your_table;更新统计信息。 - 检查
ANALYZE输出中actual time最高的节点,这就是性能瓶颈。 - 如果怀疑有隐式转换,紧跟
EXPLAIN后运行SHOW WARNINGS,查看Message中的重写 SQL,确认是否有CAST或CONVERT函数包裹了索引列。
- 使用
- 生产环境注意事项:
- 禁止在生产环境对
UPDATE/DELETE/INSERT语句使用EXPLAIN ANALYZE,因为它会真实修改数据。 - 对生产环境的
SELECT语句使用EXPLAIN ANALYZE也需谨慎,因为它会消耗真实资源,可能影响线上服务。最佳实践是将其SELECT部分复制到隔离的只读副本或测试环境执行。 - 如果必须在生产环境诊断,优先使用
Performance Schema和慢查询日志。慢查询日志中的Rows_examined字段也能提供接近于真实扫描行数的信息。
- 禁止在生产环境对
- 优化案例:
- 问题:
EXPLAIN显示rows=10,EXPLAIN ANALYZE显示rows=1,000,000。优化器以为只需要扫描 10 行,选择了 Nested Loop Join,但实际扫描了 100 万行,导致内层循环执行 100 万次,性能极差。 - 解决方案:
- 运行
ANALYZE TABLE ...;更新统计信息。 - 如果数据分布极不均匀,考虑在 MySQL 8.0+ 中创建直方图:
ANALYZE TABLE ... UPDATE HISTOGRAM ON ...;。 - 作为最后手段,使用优化器提示(如
STRAIGHT_JOIN)强制改变连接顺序。
- 运行
- 问题:
常见追问与延伸题目
-
追问:如果
EXPLAIN ANALYZE显示优化器的行数估算严重失准,你有哪些手段去纠正它?- 回答要点 1 (更新统计信息):首先尝试执行
ANALYZE TABLE tbl_name;。这是最直接、最常规的方法,它会重新采样并更新表的基数和索引的统计数据,优化器下次就会基于更新后的信息做决策。 - 回答要点 2 (调整采样率):如果默认的采样率不足以反映真实数据分布,可以调整
innodb_stats_persistent_sample_pages参数,增加采样页数,让统计信息更精确,然后再次运行ANALYZE TABLE。 - 回答要点 3 (使用直方图):对于数据分布极不均匀的列(如状态列,99% 的订单是“完成”,1% 是“待支付”),仅靠基数统计是不够的。在 MySQL 8.0+,可以为这些列创建直方图
ANALYZE TABLE tbl_name UPDATE HISTOGRAM ON col_name;,让优化器了解值的分布情况,从而做出更精准的行数估算。
- 回答要点 1 (更新统计信息):首先尝试执行
-
追问:你不能在生产库上对一个非常慢的
UPDATE语句执行EXPLAIN ANALYZE,你该如何诊断它的性能问题?- 回答要点 1 (分析
SELECT部分):将UPDATE tbl SET ... WHERE condition转换为SELECT ... FROM tbl WHERE condition。这个SELECT语句的WHERE部分和UPDATE的访问路径是完全相同的。可以在只读副本或测试环境对这个SELECT安全地执行EXPLAIN和EXPLAIN ANALYZE,分析其索引使用和真实扫描行数。 - 回答要点 2 (利用 Performance Schema):在生产环境,开启
Performance Schema的statements和stages相关监控。执行UPDATE后,查询events_statements_history_long和events_stages_history_long表。这可以告诉你查询在各个阶段(如optimizing,executing,Innodb engine,row lock wait)的精确耗时,从而定位是卡在 CPU 计算、I/O 还是锁等待。 - 回答要点 3 (检查锁信息):
UPDATE慢的常见原因是锁等待。通过SHOW ENGINE INNODB STATUS查看LATEST DETECTED DEADLOCK和TRANSACTIONS部分,或者查询information_schema下的INNODB_TRX,INNODB_LOCKS,INNODB_LOCK_WAITS表,可以明确看到查询是否在等待其他事务释放行锁。
- 回答要点 1 (分析
-
追问:PostgreSQL 也有
EXPLAIN ANALYZE,它和 MySQL 的版本相比,有什么更强大的地方?- 回答要点 1 (成熟度和历史):PostgreSQL 的
EXPLAIN ANALYZE功能历史悠久,非常成熟。而 MySQL 是在 8.0 版本后期才引入,相对较新。 - 回答要点 2 (Buffer/IO 统计):PostgreSQL 有一个杀手级特性:
EXPLAIN (ANALYZE, BUFFERS)。执行后,它不仅显示时间、行数,还会显示每个节点的缓冲区使用情况(Buffers: shared hit=..., shared read=...)。shared hit表示数据块在共享内存(Buffer Pool)中命中,shared read表示需要从磁盘读取。这使得诊断 I/O 瓶颈变得极其直观和简单,这是 MySQLEXPLAIN ANALYZE目前不具备的。 - 回答要点 3 (其他详细信息):PostgreSQL 的
EXPLAIN选项更丰富,例如(ANALYZE, TIMING)可以分别显示每个节点的启动和运行时间,(ANALYZE, WAL)可以显示WAL(预写日志)记录生成量,SETTINGS可以显示影响查询计划的非默认配置参数。总体而言,PostgreSQL 在执行计划分析的精细度和深度上通常优于 MySQL。
- 回答要点 1 (成熟度和历史):PostgreSQL 的