1. 场景还原
那天周五晚上 10:47,支付回调接口突然变慢。网关 2 秒超时,订单一直停在 PENDING。 我打开 MySQL 慢日志,发现刷屏的全是同一句 SQL:
SELECT p.id, p.order_no, p.amount, p.status, p.payment_provider, p.created_at
FROM pending_payments p
WHERE p.payment_provider = 'alipay'
AND p.status = 'PENDING'
AND p.created_at < NOW() - INTERVAL 2 MINUTE
ORDER BY p.created_at DESC
LIMIT 1000;
同事第一反应是“索引不够,加个索引”。我没让他动。
因为我同时开着 MySQL Workbench 8.0.36 的 Performance Reports,看到数据库整体负载不高,但 wait/io/table/sql/handler 这个等待事件占了 79% 的总等待时长。
这不是锁竞争,是同一张表被反复扫描回表。直接加普通索引也能缓解,但加错索引会把排序问题留到后面。
最后加了联合索引,p95 从 462ms 降到 21ms。这篇文章不讲玄学,只讲可复现的操作。
2. 方案对比:慢日志 vs Workbench 性能报告
2.1 传统慢日志方案
传统做法是三步:
- 打开
slow_query_log,设置long_query_time - 等一段时间,用
mysqldumpslow聚合 - 拿 TOP SQL 去做
EXPLAIN
这个流程的问题很明显:慢日志只记录 SQL 文本,不记录执行期间的等待事件。 你看到一条 SQL 花了 2 秒,但不知道 1.8 秒花在 IO、锁等待还是排序。 遇到“单条不慢,但每秒跑几千次”的 SQL,慢日志甚至不会记录,因为单次耗时没超过阈值。
2.2 Workbench 性能报告方案
MySQL Workbench 的 Performance Reports 底层读的是 performance_schema 和 sys schema 的统计表。
它把数据库内部计数器变成可视化报告:等待事件、语句延迟、表 IO 统计、锁等待,全都有。
关键区别:慢日志告诉你“哪条 SQL 慢”,Workbench 告诉你“为什么慢”。
2.3 对比表
| 维度 | 慢日志 + mysqldumpslow | Workbench Performance Reports |
|---|---|---|
| 看到等待事件 | 不能,只有 SQL 文本 | 能,wait/io、wait/lock、sort |
| 高频 SQL 统计 | 要等阈值触发 | 有 events_statements_summary_by_digest |
| 锁等待分析 | 很难 | 能看 lock wait 事件 |
| 历史数据 | 只要文件不删就有 | 只有本地快照,重启后计数器清零 |
| 性能开销 | 低,但写日志频繁也卡 IO | 有,需要控制 instrument 范围 |
结论:生产排查我建议两个都开。慢日志留证据,Workbench 报告做定位。
3. 前置配置
3.1 版本环境
- MySQL:8.0.35,Linux x86_64
- MySQL Workbench:8.0.36
- PHP:8.3,CLI,pdo_mysql + pcntl
- 压测机:4 核 8G,和 MySQL 同机
3.2 my.cnf 配置
Workbench Performance Reports 依赖 performance_schema。
MySQL 8.0 默认开启,但很多云数据库会关掉。先确认:
$ mysql -uroot -p -e "SELECT @@performance_schema, @@slow_query_log, @@long_query_time;"
+--------------------+------------------+-------------------+
| @@performance_schema | @@slow_query_log | @@long_query_time |
+--------------------+------------------+-------------------+
| 1 | 1 | 0.05 |
+--------------------+------------------+-------------------+
如果第一列是 0,修改 /etc/mysql/mysql.conf.d/mysqld.cnf:
[mysqld]
performance_schema=ON
performance_schema_instrument='wait/%=ON'
performance_schema_consumer_events_statements_history_long=ON
performance_schema_consumer_events_waits_history_long=ON
slow_query_log=ON
slow_query_log_file=/var/log/mysql/mysql-slow.log
long_query_time=0.05
log_queries_not_using_indexes=1
然后重启:
sudo systemctl restart mysql
mysql -uroot -p -e "SELECT @@performance_schema, @@long_query_time;"
long_query_time=0.05 是测试环境用的,生产环境建议 1 秒以上,否则慢日志文件很快爆炸。
4. 制造故障:一个能直接跑的 PHP 压力脚本
我用一张 230 万行的 pending_payments 表做实验。
优化前,表上只有 idx_status(status)。
执行计划走了 idx_status,然后回表 + filesort,每次要扫 187492 行。
下面脚本会 fork 出 10 个并发进程,总共跑 5000 次查询:
<?php
declare(strict_types=1);
$dsn = getenv('DB_DSN') ?: 'mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASS') ?: 'secret';
$workers = (int)(getenv('WORKERS') ?: 10);
$total = (int)(getenv('TOTAL') ?: 5000);
$per = intdiv($total, $workers);
$pids = [];
for ($i = 0; $i < $workers; $i++) {
$pid = pcntl_fork();
if ($pid === -1) {
fwrite(STDERR, "fork failed\n");
exit(1);
}
if ($pid === 0) {
run_queries($dsn, $user, $pass, $per, $i);
exit(0);
}
$pids[] = $pid;
}
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
echo "all workers done\n";
function run_queries(string $dsn, string $user, string $pass, int $count, int $workerId): void
{
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$sql = <<<SQL
SELECT p.id, p.order_no, p.amount, p.status, p.payment_provider, p.created_at
FROM pending_payments p
WHERE p.payment_provider = 'alipay'
AND p.status = 'PENDING'
AND p.created_at < NOW() - INTERVAL 2 MINUTE
ORDER BY p.created_at DESC
LIMIT 1000
SQL;
$start = microtime(true);
for ($i = 0; $i < $count; $i++) {
$stmt = $pdo->query($sql);
while ($stmt->fetch()) {
// 模拟业务处理,不缓存结果集
}
if ($i % 100 === 0) {
fwrite(STDOUT, sprintf("[worker %d] %d/%d\n", $workerId, $i, $count));
}
}
$elapsed = microtime(true) - $start;
fwrite(STDOUT, sprintf("[worker %d] finished in %.2fs\n", $workerId, $elapsed));
}
跑法:
WORKERS=10 TOTAL=5000 php simulate_slow.php
跑完以后,慢日志里已经有记录。先看一眼传统聚合结果:
mysqldumpslow -s t /var/log/mysql/mysql-slow.log | head -20
5. Workbench 操作流程
5.1 打开 Performance Reports
在 MySQL Workbench 8.0.36 中:
- 连接实例后,点顶部菜单 Server -> Performance Reports
- 左侧导航选 Wait Events
- 看 Top Wait Events 或 Global Wait Events
这个界面不是实时性能监控。它显示的是“当前累计值”。 想看增量,点右侧 Refresh,记录两组数字之间的差值。
5.2 三个关键视图
- Wait Events:直接告诉你时间花在 IO、锁、排序还是网络。
- Statement Latency:按 SQL 摘要聚合,不用等慢日志。
- Table I/O:看哪张表 IO 最重,配合索引优化。
当时我看到的 Top Wait Event:
| Event | Waits | Total Wait Time | 占比 |
|---|---|---|---|
| wait/io/table/sql/handler | 1,924,156 | 12.37s | 79% |
| wait/io/socket/sql/client_connection | 382,641 | 1.02s | 6.5% |
| idle | - | - | 忽略 |
这说明瓶颈是 InnoDB 层反复读数据页,不是锁等待,也不是 CPU 不够。
如果是 wait/table/lock 这类事件很高,才应该去看锁。
6. 把报告变成可复用的 SQL
Workbench 界面只是把下面的性能表画成图。你自己也可以直接查:
6.1 查询 Top 等待事件
SELECT EVENT_NAME,
COUNT_STAR AS waits,
ROUND(SUM_TIMER_WAIT / 1e12, 2) AS total_wait_s,
ROUND(AVG_TIMER_WAIT / 1e12, 4) AS avg_wait_s
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE EVENT_NAME NOT IN ('idle', 'wait/io/socket/sql/client_connection')
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
6.2 查询历史中最近的锁等待
如果你怀疑锁,直接查 history_long:
SELECT e.THREAD_ID,
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_DB,
t.PROCESSLIST_QUERY,
e.EVENT_NAME,
ROUND(e.TIMER_WAIT / 1e12, 3) AS wait_s
FROM performance_schema.events_waits_history_long e
JOIN performance_schema.threads t
ON t.THREAD_ID = e.THREAD_ID
WHERE e.EVENT_NAME LIKE 'wait/table%lock%'
OR e.EVENT_NAME LIKE 'wait/lock%'
ORDER BY e.TIMER_WAIT DESC
LIMIT 20;
6.3 查询 SQL 摘要,按总耗时排序
SELECT SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR AS exec_count,
ROUND(SUM_TIMER_WAIT / 1e12, 2) AS total_s,
ROUND(AVG_TIMER_WAIT / 1e12, 3) AS avg_s,
SUM_ROWS_EXAMINED AS rows_examined,
SUM_ROWS_SENT AS rows_sent,
ROUND(SUM_ROWS_EXAMINED / NULLIF(SUM_ROWS_SENT, 0), 1) AS rows_examined_per_sent
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'shop'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
优化前的结果:
| SQL | 执行次数 | 总耗时 | rows_examined | rows_sent | examined/sent |
|---|---|---|---|---|---|
| SELECT ... pending_payments ... | 5000 | 386.21s | 937,460,000 | 5,000,000 | 187.5 |
每条查询平均要扫描 18.7 万行,只返回 1000 行。 这种 SQL 用慢日志定位耗时,再用 Workbench 报告看等待事件,逻辑才完整。
7. 效果数据
定位后做的改动只有一个:
ALTER TABLE pending_payments
ADD INDEX idx_provider_status_created(payment_provider, status, created_at);
为什么这么建:
- 查询条件是 payment_provider 和 status 等值
- 排序字段是 created_at
- 联合索引让排序不需要 filesort,减少回表
改动前后压测数据:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 单次查询 p95 | 462ms | 21ms |
| 单次查询 p99 | 688ms | 38ms |
| 平均 rows_examined | 187,492 | 15 |
| 5000 次查询总耗时 | 386.21s | 13.85s |
| wait/io/table/sql/handler 总量 | 12.37s | 0.31s |
| 慢日志条数/小时 | 3100 | 12 |
| MySQL CPU 使用率 | 82% | 23% |
注意:这里压测环境和 MySQL 同机,数字是相对值。 你要在自己环境重新跑一遍,不要直接拿我的数据写汇报。
8. 避坑
坑 1:performance_schema 被云厂商关了
Workbench 报告打开全空白,90% 是这个原因。 先在 MySQL 里执行:
SHOW VARIABLES LIKE 'performance_schema';
如果是 OFF,不要试 SET GLOBAL performance_schema = ON,那是只读变量,会直接报错。
只能在 my.cnf 里改并重启。如果是云数据库 RDS,去控制台参数组里找 performance_schema。
坑 2:只开 performance_schema 还不够,instrument / consumer 没开
我遇到过 Performance Schema 显示 ON,但报告里 event 全是空的。 因为 setup_instruments 和 setup_consumers 可能被关闭。执行:
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/io/%'
OR NAME LIKE 'wait/lock/%';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME IN (
'events_waits_current',
'events_waits_history',
'events_waits_history_long',
'events_statements_history_long'
);
注意:这个操作会开启大量埋点,高并发生产环境要先评估。 建议只在排查窗口期开启,排查完以后关掉部分 instrument,或者用 sys schema 的存储过程:
CALL sys.ps_setup_disable_instrument('wait/io/socket/%');
CALL sys.ps_setup_disable_consumer('events_statements_history_long');
坑 3:long_query_time 设太小,慢日志变成磁盘杀手
我见过有人把 long_query_time 设成 0,结果 3 小时慢日志文件写满磁盘,数据库直接只读。
正确做法:
- 生产环境:先设 1 秒,观察一天
- 如果在排查某几条已知 SQL:设 0.1,只开 30 分钟
- 排查完马上改回去,别留着
坑 4:Workbench Performance Reports 没有长期历史
这个报告读的是内存计数器。数据库重启后,所有累计值归零。
它不是 Prometheus/Grafana,不能看三天前的趋势。
需要长期监控的话,自己定期把 performance_schema 表数据采集到本地,再画图。
坑 5:用 root 连 Workbench 不安全
给监控用专门账号,最小权限:
CREATE USER 'wb_mon'@'%' IDENTIFIED BY 'strong-password';
GRANT SELECT ON performance_schema.* TO 'wb_mon'@'%';
GRANT SELECT ON sys.* TO 'wb_mon'@'%';
不要给全局 ALL PRIVILEGES。
Workbench 连接只读监控账号就可以了,避免误操作线上数据。
9. 总结
慢查询排查,我的固定流程就三条:
- Workbench Performance Reports 看等待事件,区分 IO / 锁 / 排序
- performance_schema 的 statement digest 找高频和高耗时 SQL
- 用 rows_examined / rows_sent 比值决定加什么索引
慢日志仍然是很好的留底工具,但它不够“根因”。 下一次数据库卡顿,先别急着加索引,打开 Workbench 的报告,把“为什么慢”搞清楚再动手。