Workbench性能报告扫雷:慢查询3步定位
发布日期: 2026/08/09 阅读总量: 0

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_schemasys 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 EventsGlobal 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 的报告,把“为什么慢”搞清楚再动手。