服务器性能测试中识别MySQL慢查询的核心指标
本文面向IT运维工程师,提供在服务器压测中定位MySQL慢查询根因的实操路径:先固定测试条件和QPS口径,再采集慢日志与连接层数据联合对比,判断是SQL索引问题还是连接池排队,并用复测确认优化方向。

先判断“慢的是执行”还是“慢的是等待”,再谈索引或连接池
压测里常见这种现象:QPS下滑了,慢日志告警在增,但 CPU 并没打满。很多人第一反应会把 SQL 语句拿去补索引,结果跑完两三轮才发现波动仍在,甚至更明显。 更准确的说法是:在服务器性能测试中,MySQL慢查询并不自动等于索引问题,尤其当线程端口被占满、应用层连接池在排队时,慢日志看到的只是“慢着执行完的尾巴”。
要让结论成立,先固定三个条件:测试阶段不迁移数据、不改主机配置(CPU、磁盘、内存比例不变)、压测脚本和并发固定。只要口径不变,才有意义区分“慢查询慢”和“慢连接慢”。
指标定义:先把读法统一,再做判断
先抓的核心字段(慢日志)
在 MySQL慢查询 复盘里,最先看的不是“慢到几秒”,而是与锁与扫描量的关系:
| 指标 | 来源 | 为什么重要 |
|---|---|---|
| Query_time | 慢日志 | 语句执行耗时主入口,决定是否是执行路径问题 |
| Lock_time | 慢日志 | 行/表级等待时间,偏大常提示锁争用 |
| Rows_examined | 慢日志 | 实际扫描行数,决定索引是否命中 |
| Rows_sent | 慢日志 | 返回行数,和 Rows_examined 一起看扫描效率 |
| Rows_affected | 慢日志 | 写入类语句的写放大参考 |
| rows_ratio = Rows_examined / Rows_sent | 计算字段 | 判断是否存在“大量无效扫描” |
| 慢日志总数/时间窗口 | 慢日志 | 观察是否是“少量重问题”还是“广泛退化” |
rows_ratio 很关键。Rows_examined 与 Rows_sent 相差很大,通常说明同样结果用掉了过多扫描。即使 Query_time 不算特别离谱,也可能是索引缺失或条件不当造成。
同步看服务器侧连接状态(决定是否是连接池问题)
连接层指标可用来判断是否存在排队与抢占:
| 指标 | 来源(性能测试常用) | 关键观察 |
|---|---|---|
| Questions | performance_schema.global_status |
计算 QPS 的原始计数 |
| Threads_connected | performance_schema.global_status |
已建立连接数 |
| Threads_running | performance_schema.global_status |
当前运行线程数 |
| Max_used_connections | performance_schema.global_status |
历史峰值,是否长期触顶 |
| Aborted_connects | performance_schema.global_status |
连接异常数,池配置与认证波动参考 |
| Innodb_row_lock_waits / wait_time | performance_schema.global_status 或 SHOW ENGINE INNODB STATUS |
与锁相关瓶颈和等待时延 |
| 连接池 pending/queue | 应用监控(如 HikariCP) | 判断是否拿不到连接 |
Threads_running 长期接近上限、且 pending 长时间>0 时,慢查询多半不是“单条SQL失控”,而是“连接与线程排队”。
QPS 口径统一
QPS 不建议直接看“某一秒”瞬时值,建议按窗口算:
QPS = (Questions_t2 - Questions_t1) / (t2 - t1)
保持这个口径能把慢日志窗口和负载窗口对齐,避免误判。
测试环境与工具:先把实验条件固定下来
以下示例按 Linux + MySQL 8.0+ 给出,低版本或 MariaDB 可有字段差异,请先核对版本和变量名。
mysql -uroot -p -Nre "SELECT VERSION();"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'log_output'; SHOW VARIABLES LIKE 'long_query_time';"
# 压测窗口建议用 FILE 或 TABLE 都可,但建议 TABLE 便于按 SQL_text 聚合
SET GLOBAL slow_query_log = ON;
SET GLOBAL log_output = 'TABLE';
SET GLOBAL long_query_time = 0.5; -- 按业务目标可调整
SET GLOBAL min_examined_row_limit = 100; -- 过滤明显小查询,减少噪声
SET GLOBAL log_queries_not_using_indexes = ON; -- 诊断窗口短期开启
-- 采集基础状态基线
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME IN ('Questions','Threads_running','Threads_connected','Max_used_connections','Aborted_connects')
ORDER BY VARIABLE_NAME;
压力工具用 sysbench、mysqlslap、内部压测脚本都可以,关键是可重复。示例用 sysbench:
sysbench --db-driver=mysql --mysql-user=sbtest --mysql-password='***' --mysql-db=sbtest \
--tables=8 --table-size=100000 --threads=64 --time=300 --report-interval=5 run
压测前后分别保留慢日志、状态和 QPS 快照,不能只看一次点位。
# 每 10 秒记录一次状态(实际建议用监控系统持久化)
while true; do
mysql -uroot -p -N -e "
SELECT NOW(),
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Questions') AS Questions,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_running') AS Threads_running,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_connected') AS Threads_connected;"
sleep 10
done
执行流程:可复现、可回放的一轮分析
- 先跑 1~2 分钟基线,确认无外部批任务/备份/监控同步任务干扰。
- 再开启压测并固定脚本参数,观察至少 5 分钟以上,让缓存、连接池达到稳定。
- 结束后先不改参数,先做分析:
-- 慢查询统计(表输出模式)
SELECT
COUNT(*) AS sample_cnt,
ROUND(AVG(query_time),4) AS avg_query_time_s,
ROUND(AVG(lock_time),4) AS avg_lock_time_s,
ROUND(SUM(rows_examined)/NULLIF(SUM(rows_sent),0),2) AS examined_ratio,
LEFT(sql_text,180) AS sql_sample
FROM mysql.slow_log
WHERE start_time >= NOW() - INTERVAL 10 MINUTE
GROUP BY sql_sample
ORDER BY avg_query_time_s DESC
LIMIT 20;
-- 性能快照与执行摘要对齐(更稳定)
SELECT
DIGEST_TEXT,
COUNT_STAR AS exec_cnt,
ROUND(SUM_TIMER_WAIT/1e9/NULLIF(COUNT_STAR,0),2) AS avg_exec_ms,
ROUND(SUM_LOCK_TIME/1e9/NULLIF(COUNT_STAR,0),2) AS avg_lock_ms,
ROUND(SUM_ROWS_EXAMINED/NULLIF(COUNT_STAR,0),2) AS avg_rows_examined,
ROUND(SUM_ROWS_SENT/NULLIF(COUNT_STAR,0),2) AS avg_rows_sent
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME='sbtest'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;
- 对同一时段对齐采样,补充连接池监控(应用端)确认
pending/active与数据库Threads_running的趋势是否同步。 - 只变更一个方向:这轮只改索引或只改连接池,不要两者同时动。
异常结果解释:如何选择优先动作
结果指向索引优化
满足以下组合时,优先做 SQL 与索引核查:
examin_ratio持续显著高,且大头 SQL 一样;Query_time提高,Lock_time不一定高;Threads_running没有长期高位贴近上限;- 同步看
performance_schema,问题集中在少数几个 digest 上。
这时动作顺序是:
- 用
EXPLAIN/EXPLAIN ANALYZE查执行计划; - 先确认过滤列、排序列、连接列是否被联合索引覆盖;
- 在测试环境先加索引或重写谓词;
- 重跑同一窗口,观察
examined_ratio、avg_query_time和 QPS 是否同步改善。
结果指向连接池优先
满足以下组合时,优先看连接池:
- QPS 波动但
Thread_running与Threads_connected同步高位; - 应用侧出现连接等待(pending/queue);
- 慢日志
lock_time不高、Rows_examined也没显著扩大; - 同一 SQL 在慢日志里的频率不一定变化,但响应抖动明显。
这时动作顺序是:
- 先核对连接池
maxPoolSize、空闲回收、连接泄漏与超时; - 校准数据库
max_connections与连接池上限比例(避免数据库侧连接被拖死); - 检查健康检查与保活查询是否占用执行线程;
- 再重测,确认
Threads_running峰值回落且pending消失。
两者都在退化时
若 query_time 也高、lock_time 也高、Threads_running 也高,说明“SQL与连接层”可能同时受压。此时不能用一句话归因,必须先改单一变量做 A/B,否则看起来“全部变好”只是抵消误差。
影响因素:哪些情况会让判断偏差
long_query_time设得过高会漏掉 100~300ms 区间的关键语句;建议在测试时先降到可见阈值,再恢复。- 缓存未热:全量冷读会把
Rows_examined推高,第一次测试误判更大。 - 短连接风暴:频繁建连会放大
Threads_connected,但根因可能在应用超时/重连逻辑。 - 锁等待:
Lock_time和Innodb_row_lock_waits上升时,索引再好也可能拖不动。 - 读写混合、复杂聚合、排序分页类语句天生会有较高
Rows_examined,先按语句类型分桶,不要只盯全局平均。
结果解释边界与复测方式
复测时至少做两个“可比轮次”,参数完全一致:同样数据量、同样并发、同样持续时间。
对比指标应包含三类:QPS、慢查询核心字段(尤其 query_time 与 rows_ratio)、连接层状态(Threads_running 与应用 pending)。只有这三类都向“更低开销/更稳定”方向时,才可将问题定义为已被修复。
如果只改善了其中一类,或者改动后波动更大,结论就不成立,直接回到对应路径做下一轮单变量排查。