一次慢 SQL 不是数据库的锅:我怎么用 EXPLAIN 和采样日志定位真正瓶颈
慢SQL不一定都是数据库的锅。这次故障排查花了2小时,但真正解决问题只用了10分钟。大部分时间都在"以为是数据库问题"的错误方向上。关键教训:遇到慢SQL,先别急着改索引,用EXPLAIN和采样日志确认瓶颈在哪一层。工具分享:我写了一个自动化采样脚本,放在GitHub了,大家可以拿去用。相关阅读凌晨2点生产库CPU飙到90%:一次PostgreSQL慢查询引发的雪崩复盘Docker 容器频繁 OO
一次慢 SQL 不是数据库的锅:我怎么用 EXPLAIN 和采样日志定位真正瓶颈
昨天凌晨,运维同事在群里@我:“二白,查一下这个慢SQL,数据库CPU飙到80%了。”
我看了一眼监控,确实有一个查询耗时3.2秒。但仔细一看,这个查询很简单,单表查询,索引也建了。
搞了两个小时,最后发现——这个慢SQL根本不是数据库的锅。
01. 问题现场:一个看起来很无辜的慢SQL
监控报警的SQL长这样:
SELECT * FROM orders
WHERE user_id = ? AND status = ?
ORDER BY created_at DESC
LIMIT 20;
索引建得挺好:
KEY idx_user_status_time (user_id, status, created_at)
按理说不应该慢。但事实是,这个查询在高峰期确实卡住了。
第一反应:索引是不是失效了?
我快速跑了个 EXPLAIN:
EXPLAIN SELECT * FROM orders
WHERE user_id = 123456 AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;
结果:
id: 1
select_type: SIMPLE
table: orders
type: ref
possible_keys: idx_user_status_time
key: idx_user_status_time
key_len: 98
ref: const,const
rows: 1523
Extra: Using where; Using index
没问题啊! 索引用上了,rows也不多,应该很快才对。
02. 第一次排查:以为是索引问题
既然 EXPLAIN 看起来正常,我怀疑是不是参数传的问题。于是直接在数据库里手动执行:
# 直接在MySQL客户端执行
mysql> SELECT * FROM orders
-> WHERE user_id = 123456 AND status = 'PAID'
-> ORDER BY created_at DESC
-> LIMIT 20;
Empty set (0.02 sec)
0.02秒,飞快。
这就尴尬了。SQL本身没问题,那为什么应用里会慢?
03. 第二次排查:采样日志揪出真凶
我打开了应用的慢查询日志采样。我们有个脚本会定期采集应用侧的查询耗时:
#!/bin/bash
# slow_query_sampler.sh
# 每5秒采样一次慢查询日志
LOG_FILE="/var/log/mysql/slow.log"
SAMPLE_INTERVAL=5
while true; do
echo "=== $(date '+%Y-%m-%d %H:%M:%S') ==="
# 提取最近5秒的慢查询
tail -n 100 "$LOG_FILE" | grep "Query_time" | tail -5
sleep "$SAMPLE_INTERVAL"
done
运行脚本后,我发现了一个有趣的现象:
同样的查询,在应用侧的耗时分布非常不均匀。
=== 2026-04-09 03:15:00 ===
Query_time: 3.214s Rows_sent: 0 Rows_examined: 1523
Query_time: 0.023s Rows_sent: 20 Rows_examined: 20
Query_time: 2.876s Rows_sent: 0 Rows_examined: 1523
Query_time: 0.018s Rows_sent: 20 Rows_examined: 20
关键发现:当 Rows_sent: 0 时,查询耗时飙升到3秒;而当 Rows_sent: 20 时,只需要0.02秒。
这说明什么?
04. 真相大白:不是数据库慢,是网络和应用层的问题
进一步分析应用代码,我发现了问题的根源:
// 问题代码示例
public List<Order> getOrders(Long userId, String status) {
String sql = "SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC LIMIT 20";
// 问题1:每次都重新建立连接
Connection conn = dataSource.getConnection();
// 问题2:没有使用PreparedStatement缓存
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setLong(1, userId);
stmt.setString(2, status);
ResultSet rs = stmt.executeQuery();
// 问题3:结果集处理不当
List<Order> orders = new ArrayList<>();
while (rs.next()) {
Order order = new Order();
// ... 大量字段映射
orders.add(order);
}
conn.close(); // 每次都关闭连接
return orders;
}
三个致命问题:
- 连接池未复用:每次查询都新建连接,高峰期连接池耗尽
- PreparedStatement未缓存:每次都重新解析SQL
- 结果集处理效率低:大量反射和字段映射
更关键的是:当查询结果为空时(Rows_sent: 0),应用层还会做额外的检查:
if (orders.isEmpty()) {
// 触发降级逻辑,查询其他数据源
return fallbackQuery(userId, status);
}
这个 fallbackQuery 会去查另一个更慢的数据库,导致整体耗时飙升。
05. 解决方案:三步优化
第一步:启用连接池复用
// 使用HikariCP连接池
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/orders");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
HikariDataSource dataSource = new HikariDataSource(config);
第二步:缓存PreparedStatement
// 启用Statement缓存
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
第三步:优化结果集处理
// 使用更高效的结果集映射
public List<Order> getOrdersOptimized(Long userId, String status) {
return jdbcTemplate.query(
sql,
new Object[]{userId, status},
(rs, rowNum) -> new Order(
rs.getLong("id"),
rs.getLong("user_id"),
rs.getString("status"),
rs.getTimestamp("created_at")
)
);
}
优化后效果:
优化前:
Query_time: 3.214s Rows_sent: 0
Query_time: 2.876s Rows_sent: 0
优化后:
Query_time: 0.045s Rows_sent: 0
Query_time: 0.038s Rows_sent: 0
性能提升70倍。
06. 总结:慢SQL排查的正确姿势
这次排查让我学到了一个教训:慢SQL不一定都是数据库的锅。
正确的排查流程应该是:
第一步:用EXPLAIN验证索引
EXPLAIN SELECT ...;
看 key 和 rows,确认索引是否生效。
第二步:用采样日志分析耗时分布
# 采样脚本
tail -f /var/log/mysql/slow.log | grep "Query_time"
关注 Rows_sent 和 Rows_examined 的比例。
第三步:排查应用层代码
- 连接池是否复用?
- PreparedStatement是否缓存?
- 结果集处理是否高效?
第四步:检查网络和中间件
- 网络延迟如何?
- 是否有代理或中间件增加了开销?
07. 写在最后
这次故障排查花了2小时,但真正解决问题只用了10分钟。大部分时间都在"以为是数据库问题"的错误方向上。
关键教训:遇到慢SQL,先别急着改索引,用EXPLAIN和采样日志确认瓶颈在哪一层。
工具分享:我写了一个自动化采样脚本,放在 GitHub 了,大家可以拿去用。
相关阅读:
更多推荐
所有评论(0)