如何在生产环境抓取一条 SQL 的真实执行计划与耗时分布?
How to capture a SQL query's actual execution plan and performance profile in a production environment?
考察要点
这道题考察的是候选人对 MySQL 性能诊断工具的熟练程度和实践经验,而不仅仅是理论知识。面试官想看你是否理解**估算执行计划(EXPLAIN)与真实执行计划(Actual Execution Plan)**的根本区别,以及是否掌握了在不严重影响生产系统的前提下,获取查询真实性能数据的能力。核心是考察你对 EXPLAIN ANALYZE、Performance Schema 和 pt-query-digest 等高级工具的运用。
核心概念
在生产环境中定位单条 SQL 的性能瓶颈,不能仅仅依赖 EXPLAIN。EXPLAIN 的结果是优化器的估算,它不实际执行 SQL,因此无法反映真实的 I/O、CPU 耗时和实际扫描行数。我们需要能获取真实执行数据的工具。
1. EXPLAIN: 估算执行计划
EXPLAIN 是最基础的工具,它展示了 MySQL 查询优化器为一条 SQL 选择的执行路径。
- 作用:在不执行查询的情况下,分析 SQL 语句,并返回优化器认为最高效的执行计划。
- 关键输出列:
type: 连接类型,从system(最优) 到ALL(全表扫描,最差)。key: 实际使用的索引。rows: 估算的需要读取的行数。Extra: 额外信息,如Using filesort,Using temporary,Using index。
- 局限性:
- 估算不准:
rows和filtered字段基于表的统计信息,可能因数据更新而严重失真。 - 无成本信息:无法显示查询的真实耗时、I/O 开销、CPU 开销。
- 无法体现缓存影响:无法知道数据页是否已在 Buffer Pool 中,这对 I/O 耗时影响巨大。
- 估算不准:
2. EXPLAIN ANALYZE: 真实执行计划与耗时 (MySQL 8.0.18+)
这是获取真实执行计划和耗时的最直接工具。它会实际执行 SQL 语句,并以树状结构返回每个执行节点的真实耗时、实际返回行数等信息。
- 语法:
sql1EXPLAIN ANALYZE SELECT ...;
- 输出解读:
text1-> Nested loop inner join (cost=1.25 rows=1) (actual time=0.050..0.052 rows=1 loops=1)2 -> Table scan on t1 (cost=1.05 rows=1) (actual time=0.021..0.022 rows=1 loops=1)3 -> Index lookup on t2 using PRIMARY (id=t1.id) (cost=0.20 rows=1) (actual time=0.025..0.026 rows=1 loops=1)
(cost=... rows=...): 优化器的估算成本和行数(同EXPLAIN)。(actual time=... rows=... loops=...): 真实执行数据。actual time=0.050..0.052:(首次返回行的耗时)..(返回所有行的总耗时),单位是毫秒。rows=1: 该节点实际返回的行数。loops=1: 该节点被执行的次数。对于嵌套循环的内层,loops会大于 1。
- 版本要求:✅ MySQL 8.0.18+ / PostgreSQL。❌ MySQL 5.7 及更早版本不支持。
- 使用场景:非常适合在开发、测试环境或生产环境的只读副本上进行精确分析。在生产主库上对写操作(
UPDATE,DELETE)使用需极其谨慎,因为它会实际修改数据。对于SELECT语句,可以包裹在事务中然后回滚:sql1BEGIN;2EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;3ROLLBACK;
3. Performance Schema: 低开销的性能数据采集
Performance Schema 是 MySQL 内置的性能监控引擎,它通过在代码中埋点的方式,以较低的开销持续收集服务器运行过程中的详细事件数据。
- 工作原理:
- Instruments (探针):在服务器代码的关键路径(如解析 SQL、打开表、排序、发送数据)上设置的采集点。
- Consumers (消费者):用于存储探针采集到的事件数据的内存表。
- 核心步骤:
- 开启配置:确保相关的
instruments和consumers已被启用。主要关注events_statements_*和events_stages_*。sql1-- 开启语句和阶段事件的收集2UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES'3WHERE NAME LIKE 'statement/sql/%' OR NAME LIKE 'stage/sql/%';45-- 开启历史记录消费者6UPDATE performance_schema.setup_consumers SET ENABLED = 'YES'7WHERE NAME LIKE '%history%'; - 执行 SQL:运行你想要分析的慢查询。
- 查询结果:从
Performance Schema的表中获取耗时分布。- 获取总览:
events_statements_history_long表记录了每条 SQL 的执行摘要。 - 获取阶段耗时:
events_stages_history_long表记录了单个 SQL 执行过程中各个阶段(如stage/sql/checking permissions,stage/sql/optimizing,stage/sql/executing,stage/sql/sending data)的耗时。sql1-- 假设你知道线程 ID (THREAD_ID) 和事件 ID (EVENT_ID)2SELECT event_name, timer_wait / 1000000000.0 AS duration_ms3FROM performance_schema.events_stages_history_long4WHERE thread_id = <your_thread_id> AND nesting_event_id = <your_statement_event_id>5ORDER BY timer_start;
- 获取总览:
- 开启配置:确保相关的
- 优势:开销相对较低,可持续开启,是生产环境监控的基石。能提供非常细粒度的耗时分解。
4. 慢查询日志 + pt-query-digest
这是一种经典的、异步的分析方法,对线上服务影响最小。
- 慢查询日志 (Slow Query Log):记录执行时间超过
long_query_time秒的 SQL。 pt-query-digest:Percona Toolkit 中的一个工具,用于分析慢查询日志、通用日志或二进制日志。- 工作流程:
- 在生产环境中开启慢查询日志,并设置一个合理的
long_query_time(例如 0.5 秒)。 - 收集一段时间的日志文件。
- 使用
pt-query-digest分析日志文件。bash1pt-query-digest /path/to/slow-query.log > report.txt
- 在生产环境中开启慢查询日志,并设置一个合理的
- 报告解读:
pt-query-digest会将结构相同、仅参数不同的 SQL 归为一类(通过 Digest),并提供详尽的统计报告,包括:- Query Time: 总执行时间、平均时间、95% 分位时间。
- Lock Time: 等待锁的时间。
- Rows Sent / Rows Examined: 发送给客户端的行数与存储引擎扫描的行数,二者比值是关键优化指标。
EXPLAIN示例: 报告中会包含该类 SQL 的一个EXPLAIN结果。
常见低分回答
- 只知道
EXPLAIN:这是最常见的低分回答。当被问及“真实耗时”时,如果只回答EXPLAIN,说明候选人缺乏实际的性能调优经验,混淆了估算计划和实际执行。 - 在生产主库随意使用
EXPLAIN ANALYZE:直接说在生产库上对任意 SQL 执行EXPLAIN ANALYZE,而未提及对写操作的风险或使用事务回滚的技巧,会显得对生产环境缺乏敬畏心。 - 不知道
Performance Schema:对于一个资深 DBA 或后端工程师,不了解Performance Schema意味着其知识体系停留在比较旧的 MySQL 版本,缺乏对现代 MySQL 监控工具的了解。 - 混淆
Rows Examined和Rows Sent:在分析慢查询时,无法清晰解释这两个指标的区别和重要性。Rows Examined远大于Rows Sent通常意味着存在索引问题或查询逻辑不佳。 - 认为
EXPLAIN的rows字段是精确的:强调EXPLAIN的rows是精确的扫描行数,这是一个概念性错误。
工程实践与优化
- 工具组合拳:
- 日常巡检:通过
pt-query-digest定期分析慢查询日志,发现共性的慢查询问题。 - 问题复现:在只读副本或测试环境中,使用
EXPLAIN ANALYZE对pt-query-digest发现的典型慢查询进行精确分析,获取其真实的执行计划和耗时分布。 - 深度诊断:如果问题难以复现或与锁、临时表等瞬时状态有关,可以在生产环境临时开启
Performance Schema的相关instruments,抓取特定会话的阶段耗时信息。
- 日常巡检:通过
EXPLAIN ANALYZE的安全使用:- 优先在只读副本上执行,避免影响主库。
- 对
SELECT语句,使用BEGIN; ... ROLLBACK;将其包裹,确保无副作用。 - 对
UPDATE/DELETE,如果必须在主库分析,可先SELECT出要修改的行的主键,然后在事务中对单条记录执行EXPLAIN ANALYZE,并立即ROLLBACK。
Performance Schema的开销管理:- 生产环境不要开启所有的
instruments,按需开启。 - 诊断结束后,及时关闭不再需要的
instruments和consumers,以减少性能开销。 - 监控
Performance Schema本身的内存占用 (SHOW ENGINE PERFORMANCE_SCHEMA STATUS;)。
- 生产环境不要开启所有的
- 关注执行计划的稳定性:有时同一条 SQL 在不同时间执行计划会变化。可以使用
optimizer hints(如USE INDEX,FORCE INDEX)或创建 SQL Plan Management (SPM) 基线来固定高效的执行计划。
常见追问与延伸题目
-
追问:
EXPLAIN和EXPLAIN ANALYZE的结果不一致,以哪个为准?为什么会不一致?- 回答要点:
- 以
EXPLAIN ANALYZE为准,因为它展示的是真实的执行情况。 - 不一致的原因:
- 统计信息陈旧:
EXPLAIN依赖于表的统计信息(如Cardinality),如果数据分布发生巨大变化而未及时ANALYZE TABLE,优化器的估算(rows,cost)会严重偏离实际。 - 成本模型局限:优化器的成本模型是基于一系列固定的成本常数(如 I/O 成本、CPU 成本)进行计算的,这个模型本身就是一种近似,可能与真实的硬件和负载情况不符。
- 缓存命中率:
EXPLAIN无法预知执行时数据页是否在 Buffer Pool 中。EXPLAIN ANALYZE第一次执行可能涉及大量物理 I/O,而第二次执行可能因为数据已在缓存中而变得飞快,其actual time会真实反映这一点。 - 执行期间的优化:某些优化(如
InnoDB的 AHI 自适应哈希索引)是在运行时动态生效的,EXPLAIN无法预知。
- 统计信息陈旧:
- 以
- 回答要点:
-
追问:如果我用的是 MySQL 5.7,没有
EXPLAIN ANALYZE,如何精确分析一条 SQL 的耗时瓶颈?- 回答要点:这是一个很好的问题,考察候选人对老版本工具的掌握。
- 方法一:
SHOW SESSION STATUS:FLUSH SESSION STATUS;清空当前会话的计数器。- 执行慢查询 SQL。
SHOW SESSION STATUS LIKE 'Handler%';查看Handler_read_*系列状态变量。Handler_read_rnd_next激增表示在进行全表扫描;Handler_read_key表示使用了索引。通过这些底层计数器可以反推出存储引擎的真实访问模式。
- 方法二:
Performance Schema(5.7 版本):MySQL 5.7 已经有Performance Schema,虽然功能不如 8.0 完善,但核心的events_stages_history_long已经存在。可以按照前述方法,开启相关instruments,然后查询该表来分析 SQL 执行的各个阶段耗时。 - 方法三:
EXPLAIN+SHOW WARNINGS:执行EXPLAIN后,立刻执行SHOW WARNINGS。有时优化器会对查询进行重写(例如,类型转换),SHOW WARNINGS会显示重写后的 SQL,这有助于发现一些隐式转换导致的索引失效问题。
- 方法一:
- 回答要点:这是一个很好的问题,考察候选人对老版本工具的掌握。
-
追问:在生产环境,如何安全地、低成本地开启慢查询日志来抓取问题 SQL?
- 回答要点:
- 动态设置,非永久:通过
SET GLOBAL long_query_time = 0.5;动态开启,问题定位后可调回更高值或关闭 (SET GLOBAL long_query_time = 10;)。避免直接修改配置文件并重启服务。 - 采样记录:在 MySQL 5.6+,可以使用
log_slow_rate_limit参数进行采样。例如,SET GLOBAL log_slow_rate_limit = 100;表示每 100 个满足条件的慢查询只记录 1 个,这可以在高 QPS 场景下大幅减少日志量和 I/O 开销。 - 不记录未使用索引的查询:如果只想关注那些确实很慢的查询,而不是所有未使用索引的查询,确保
log_queries_not_using_indexes是关闭的,以避免日志爆炸。 - 日志轮转 (Log Rotation):配置
logrotate或类似工具对慢查询日志进行定期轮转和压缩,防止单个日志文件过大,消耗磁盘空间。
- 动态设置,非永久:通过
- 回答要点: