如何定位慢 SQL:工具、流程与实战示例
一、什么是慢 SQL
慢 SQL 是指执行时间超过设定阈值的 SQL 语句。MySQL 默认阈值为 10 秒,生产环境通常设为 1-2 秒。慢 SQL 会导致:
- 接口响应超时
- 数据库连接池耗尽
- 影响其他正常查询(锁等待、CPU 打满)
二、定位慢 SQL 的整体流程
告警/用户反馈 → 确定慢接口 → 获取慢 SQL 文本 → EXPLAIN 分析 → 定位根因 → 制定优化方案
↑ ↓
监控大盘 ←------------------------------------ 验证效果 ←--------------------------------- 上线 ←------------------------------------ 实施优化
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
三、定位工具详解
3.1 MySQL 慢查询日志(Slow Query Log)
这是 MySQL 内置的最基础的慢 SQL 发现手段。
开启与配置
sql
-- 查看当前状态
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 动态开启(无需重启,重启后失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SET GLOBAL log_queries_not_using_indexes = ON; -- 未走索引的也记录
-- 持久化配置(my.cnf)
-- [mysqld]
-- slow_query_log = 1
-- slow_query_log_file = /var/log/mysql/slow.log
-- long_query_time = 1
-- log_queries_not_using_indexes = 1
日志内容示例
# Time: 2026-08-04T11:44:01.123456Z
# User@Host: app_user[app_user] @ 10.0.1.100 [10.0.1.100] Id: 12345
# Query_time: 5.432100 Lock_time: 0.000100 Rows_sent: 150 Rows_examined: 12800000
SET timestamp=1722769441;
SELECT orm.record_code, sdlr.logistics_code
FROM xxx_goods_record orm
INNER JOIN xxx_logistics_record sdlr
ON orm.record_code = sdlr.record_code
WHERE orm.record_code = 'xx.20260730.001856.xx';
关键字段含义:
| 字段 | 含义 | 关注点 |
|---|---|---|
| Query_time | SQL 执行耗时 | 核心指标 |
| Lock_time | 等待锁的时间 | 高说明有锁争用 |
| Rows_sent | 返回给客户端的行数 | 与 Rows_examined 对比 |
| Rows_examined | 扫描的行数 | 远大于 Rows_sent 说明有优化空间 |
使用 mysqldumpslow 汇总分析
bash
# 按平均耗时排序,取 TOP 10
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log
# 按出现次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
# 按总耗时排序(找到总影响最大的 SQL)
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
输出示例:
Count: 356 Time=4.52s (1609s) Lock=0.00s (0s) Rows=12.0 (4272), app_user[app_user]@10.0.1.100
SELECT ... FROM xxx_goods_record orm
INNER JOIN xxx_logistics_record sdlr ON orm.record_code = sdlr.record_code
WHERE orm.record_code = 'S'
3.2 EXPLAIN 执行计划
定位到慢 SQL 后,用 EXPLAIN 分析其执行路径。
基本用法
sql
EXPLAIN SELECT o.order_no, d.product_name
FROM orders o
INNER JOIN order_details d ON o.id = d.order_id
WHERE o.status = 'PAID' AND o.create_time > '2026-01-01';
输出字段详解
+----+-------------+-------+------+---------------+---------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------+--------+-------------+
| 1 | SIMPLE | o | ref | idx_status | idx_status | 62 | const | 15000 | Using where |
| 1 | SIMPLE | d | ref | idx_order_id | idx_order_id| 4 | o.id | 3 | NULL |
+----+-------------+-------+------+---------------+---------+---------+-------+--------+-------------+
type 列(访问类型,从好到差):
| 级别 | 含义 | 示例 |
|---|---|---|
| const / system | 主键或唯一索引等值查询 | WHERE id = 1 |
| eq_ref | JOIN 时被驱动表用主键/唯一索引 | JOIN b ON a.id = b.id |
| ref | 使用普通索引等值查询 | WHERE status = 'PAID' |
| range | 索引范围扫描 | WHERE id > 100 或 WHERE time BETWEEN ... |
| index | 全索引扫描(扫描整棵索引树) | 覆盖索引但无 WHERE |
| ALL | 全表扫描(最差) | 无索引可用 |
Extra 列(重点关注项):
| 值 | 含义 | 是否需要优化 |
|---|---|---|
| Using index | 覆盖索引,不回表 | ✅ 好 |
| Using where | 在存储引擎层过滤后还需在 Server 层过滤 | 一般 |
| Using filesort | 无法利用索引排序,需要额外排序 | ⚠️ 需关注 |
| Using temporary | 使用临时表(常见于 GROUP BY、DISTINCT) | ⚠️ 需关注 |
| Using join buffer | JOIN 时被驱动表无索引,使用 Block Nested Loop | ❌ 需优化 |
EXPLAIN 进阶:FORMAT=JSON
sql
EXPLAIN FORMAT=JSON SELECT ...;
JSON 格式提供更详细的成本估算(cost_info),包括 read_cost、eval_cost、prefix_cost 等,适合深度分析。
EXPLAIN ANALYZE(MySQL 8.0.18+)
sql
EXPLAIN ANALYZE SELECT o.order_no
FROM orders o WHERE o.status = 'PAID' LIMIT 100;
这会真正执行查询并返回每一步的实际耗时、实际行数(vs 预估行数),是最精确的分析方式。
3.3 Performance Schema
MySQL 内置的性能监控框架,可以统计 SQL 级别的执行信息。
sql
-- 查看执行次数最多的 TOP SQL
SELECT DIGEST_TEXT, COUNT_STAR, AVG_TIMER_WAIT/1000000000 AS avg_ms,
SUM_ROWS_EXAMINED, SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
-- 查看当前正在执行的慢查询
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 2
ORDER BY time DESC;
3.4 SHOW PROFILE(定位 SQL 内部耗时分布)
sql
-- 开启 profiling
SET profiling = 1;
-- 执行目标 SQL
SELECT * FROM orders WHERE status = 'PAID';
-- 查看概要
SHOW PROFILES;
-- 查看具体某条 SQL 的耗时分布
SHOW PROFILE FOR QUERY 1;
输出示例:
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000021 |
| checking permissions | 0.000005 |
| Opening tables | 0.000015 |
| init | 0.000018 |
| System lock | 0.000007 |
| optimizing | 0.000004 |
| statistics | 0.000012 |
| preparing | 0.000008 |
| Sorting result | 0.000003 |
| executing | 0.000001 |
| Sending data | 4.312500 | ← 瓶颈在这里:数据读取/传输
| end | 0.000004 |
| query end | 0.000003 |
| closing tables | 0.000005 |
| freeing items | 0.000012 |
+----------------------+----------+
3.5 APM(应用性能监控)系统
在微服务架构中,通常通过 APM 系统来关联 HTTP 请求与底层 SQL。
常见 APM 工具
| 工具 | 特点 |
|---|---|
| SkyWalking | 开源,Java Agent 无侵入接入,支持 SQL 参数抓取 |
| Pinpoint | 开源,韩国 Naver 出品,UI 直观 |
| Arthas (阿里) | 在线诊断工具,可动态追踪方法耗时 |
| 阿里云 ARMS | 商业版,集成告警和 SQL 分析 |
| CAT (大众点评) | 开源,实时监控 |
APM 定位流程示例(以 SkyWalking 为例)
1. 告警:接口 /xxx-logistics-info P99 > 3s
2. 打开 SkyWalking UI → Trace 查询 → 按接口路径过滤
3. 选择一条慢请求的 Trace
4. 查看 Span 瀑布图:
├─ HTTP GET /xxx-logistics-info [3200ms]
│ ├─ MyBatis: findByRecordCode [12ms]
│ ├─ MyBatis: findByMemberIdAndRecordCode [8ms]
│ └─ MyBatis: listStoreLogisticsInfoByReceivingRecordCode [3150ms] ← 瓶颈
│ └─ MySQL: SELECT orm.record_code... [3148ms]
5. 复制完整 SQL → 用 EXPLAIN 分析
从 trace-id 关联 SQL
生产环境中,SQL 前通常会注入 trace 信息:
sql
/* xxx-request-id:276077ca1786xxxx
trace-id:276077ca1786068xxxx
rpc-id:0.1.1 */
SELECT orm.record_code ...
这样在慢查询日志中可以直接通过 trace-id 关联到具体的用户请求。
3.6 数据库管理平台
企业级数据库管理平台通常集成了慢 SQL 的自动采集与分析:
| 平台 | 功能 |
|---|---|
| Archery | 开源 SQL 审核平台,集成慢日志分析 |
| DMS (阿里云) | SQL 审计、慢日志、锁分析一体化 |
| Yearning | 开源 SQL 审核,支持慢查询展示 |
| 自建 ELK | 将慢日志导入 Elasticsearch,用 Kibana 可视化 |
四、实战定位流程
场景:线上接口超时告警
Step 1:确认告警信息
告警内容:接口 /api/composite/stock/logistics/xxx-xxxx-logistics-info
P99 延迟 5.4s,超过阈值 2s
告警时间:2026-08-04 11:44
影响范围:所有调用该接口的用户
Step 2:通过 APM 定位慢 SQL
从 APM 的 Trace 详情中获取:
- 慢 SQL 全文
- 执行耗时:5.43s
- 扫描行数:12,800,000
Step 3:EXPLAIN 分析
sql
EXPLAIN SELECT orm.record_code as orderCode, ...
FROM xxx_goods_record orm
INNER JOIN xxx_logistics_record sdlr
ON orm.record_code = sdlr.record_code AND orm.member_id = sdlr.member_id
LEFT JOIN xxxx_logistics_record_item sdlri
ON sdlr.id = sdlri.store_receiving_logistics_record_id
WHERE orm.record_code = 'xxx.20260730.001856.xxx'
ORDER BY sdlr.create_time DESC, sdlri.state_time ASC;
分析结果可能显示:
receiving_goods_record走了唯一索引(type=const),本身不慢- 但 JOIN 时
store_receiving_logistics_record的关联条件涉及record_code + member_id,如果联合索引不匹配,可能产生额外扫描 - 三表 JOIN 后排序
Using filesort
Step 4:确定优化方向
通过表结构分析发现 sdlr 表本身有 record_code 和 order_code 字段,可以不关联 orm 表。
Step 5:验证数据一致性
sql
-- 确认两表字段数据完全一致
SELECT COUNT(*) FROM receiving_goods_record orm
INNER JOIN xxx_logistics_record sdlr
ON orm.record_code = sdlr.record_code AND orm.member_id = sdlr.member_id
WHERE orm.order_code != sdlr.order_code;
-- 结果: 0
Step 6:实施优化并验证
修改 SQL → 本地测试 → 灰度上线 → 观察监控指标。
五、日常巡检:主动发现慢 SQL
除了被动告警,还需要定期主动巡检:
sql
-- 1. 查看当前有哪些长事务在跑
SELECT trx_id, trx_state, trx_started, trx_query, trx_rows_locked
FROM information_schema.innodb_trx
WHERE trx_state = 'RUNNING' AND TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 5;
-- 2. 查看表的索引使用情况(哪些索引从未被使用)
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL AND count_star = 0
AND object_schema = 'your_database';
-- 3. 查看哪些表全表扫描最多
SELECT object_schema, object_name, count_read AS full_scan_count
FROM performance_schema.table_io_waits_summary_by_table
WHERE count_read > 100000
ORDER BY count_read DESC LIMIT 10;
六、总结对照表
| 场景 | 推荐工具 | 定位精度 |
|---|---|---|
| 开发/测试环境调试 | EXPLAIN + SHOW PROFILE | SQL 级别 |
| 生产环境定位 | 慢查询日志 + mysqldumpslow | SQL 级别 |
| 微服务链路追踪 | APM (SkyWalking/Pinpoint) | 请求 → SQL 全链路 |
| 实时在线诊断 | Arthas / SHOW PROCESSLIST | 即时 |
| 长期趋势分析 | Performance Schema / ELK | 统计维度 |
| 锁问题排查 | INNODB_TRX + INNODB_LOCKS | 事务/锁级别 |