MySQL慢查询定位与执行计划分析:从踩坑到起飞
某天凌晨3点,报警短信惊醒了睡梦中的你:"DB响应时间超过5秒!"你颤抖着打开电脑,发现一条SQL正以蜗牛速度啃食着数据库性能。别慌!掌握慢查询定位和执行计划分析,你也能从"救火队员"晋升为"数据库外科医生"!
一、慢查询:数据库里的"龟速快递"
什么是慢查询? 简单说就是执行时间超过预定阈值的SQL语句。就像快递中的龟速包裹,它们消耗数据库资源,拖垮系统性能。
为什么要关注?
- 1条慢查询可能堵塞数百个正常请求
- 80%的数据库性能问题由不到20%的SQL引起
- 查询速度每提升1秒,电商转化率最高可提升10%(真实研究数据!)
二、慢查询日志:犯罪现场调查
1. 开启慢查询日志
sql
-- 查看当前设置(默认关闭)
SHOW VARIABLES LIKE 'slow_query_log';
-- 开启慢查询日志(重启失效)
SET GLOBAL slow_query_log = 'ON';
-- 设置慢查询阈值(单位:秒)
SET GLOBAL long_query_time = 1;
-- 记录未使用索引的查询(强烈建议开启)
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 永久生效配置(修改my.cnf)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
2. 慢查询日志分析神器
mysqldumpslow(MySQL自带):
bash
# 统计最慢的10条SQL
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log
# 解析结果示例
Count: 5 Time=8.32s (41s) Lock=0.00s (0s) Rows=1000.0 (5000)
SELECT * FROM orders WHERE user_id = N AND status = 'PENDING'
Pt-query-digest(Percona Toolkit)更强大:
bash
pt-query-digest /var/lib/mysql/slow.log
# 输出包括:
# 响应时间占比、执行次数、锁时间、扫描行数等关键指标
三、实战案例:拯救电商平台的"慢"订单查询
场景描述
某电商平台订单列表页加载需要15秒,用户投诉激增。Java代码片段如下:
java
// 订单查询Service(问题代码)
public List<Order> getOrdersByUser(Integer userId, String status) {
String sql = "SELECT * FROM orders WHERE user_id = " + userId;
if (status != null) {
sql += " AND status = '" + status + "'";
}
return jdbcTemplate.query(sql, new OrderRowMapper());
}
问题定位
-
查看慢查询日志发现:
ini# Time: 2025-06-30T03:12:05.123456Z # Query_time: 12.345 Lock_time: 0.123 Rows_sent: 10 Rows_examined: 500000 SELECT * FROM orders WHERE user_id = 12345 AND status = 'PENDING';
-
使用EXPLAIN分析:
sqlEXPLAIN SELECT * FROM orders WHERE user_id = 12345 AND status = 'PENDING';
执行计划结果:
id select_type table type key rows Extra 1 SIMPLE orders ALL NULL 500000 Using where
死因分析
type=ALL
:全表扫描50万行key=NULL
:未使用索引Rows_examined=500000
:实际扫描所有行
手术方案
步骤1:添加复合索引
sql
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
步骤2:优化Java代码(防止SQL注入)
java
public List<Order> getOrdersByUser(Integer userId, String status) {
String sql = "SELECT * FROM orders WHERE user_id = ?";
List<Object> params = new ArrayList<>();
params.add(userId);
if (status != null) {
sql += " AND status = ?";
params.add(status);
}
return jdbcTemplate.query(
sql,
params.toArray(),
new OrderRowMapper()
);
}
优化后执行计划:
id | select_type | table | type | key | rows | Extra |
---|---|---|---|---|---|---|
1 | SIMPLE | orders | ref | idx_user_status | 10 | Using where |
效果对比:
- 查询时间:12.3s → 0.02s(提升615倍!)
- 扫描行数:500000 → 10
四、执行计划深度解码
EXPLAIN输出列详解
列名 | 武功秘籍 | 致命陷阱 |
---|---|---|
type | 访问类型(性能排序): | ALL全表扫描是性能杀手 |
system > const > eq_ref > ref > range > index > ALL | ||
key | 实际使用的索引 | 出现NULL表示索引失效 |
rows | 预估扫描行数 | 实际值可能偏差10倍! |
Extra | 附加信息 | 出现这些立即警报: |
Using filesort (文件排序) |
||
Using temporary (临时表) |
索引失效的七种暗器
-
索引字段使用函数
WHERE YEAR(create_time) = 2025
→ 改用范围查询 -
隐式类型转换
WHERE user_id = '10001'
(user_id是int)→ 移除引号 -
模糊查询左通配
WHERE product_name LIKE '%手机%'
→ 尽量用右通配 -
OR条件未全覆盖
WHERE a=1 OR b=2
→ 为a、b分别建索引不如建联合索引 -
最左前缀原则违反
索引
(a,b,c)
,查询WHERE b=1 AND c=2
→ 失效 -
索引列参与计算
WHERE price*2 > 100
→ 改为price > 50
-
范围查询后的列失效
WHERE a>1 AND b=2
→ 只有a用索引,b失效
五、避坑指南:血泪经验总结
慢查询日志三大坑
-
日志爆盘风险
对策:定期清理 + 设置
max_binlog_size
-
阈值设置不当
建议:从1秒开始,逐步调整到500ms
-
云数据库权限限制
阿里云/RDS需在控制台开启慢查询日志
执行计划误区
-
❌ "rows列数值小就一定快"
→ 可能遇到索引统计信息过时,用
ANALYZE TABLE
刷新 -
❌ "Extra出现Using index就万事大吉"
→ 覆盖索引虽好,但排序和筛选仍需警惕
-
❌ "强制索引force index能解决所有问题"
→ 可能导致更差性能,应是最后手段
六、最佳实践:数据库性能调优九阳神功
-
监控常态化
sql-- 实时查看慢查询 SHOW FULL PROCESSLIST; -- 每周执行索引健康检查 SELECT * FROM sys.schema_unused_indexes;
-
SQL编写军规
- 禁用
SELECT *
- 批量操作代替循环
- 用
UNION ALL
替代UNION
(不去重)
- 禁用
-
连接池优化(Java示例)
java// HikariCP配置(比Druid性能高30%) HikariConfig config = new HikariConfig(); config.setMaximumPoolSize(20); // 不是越大越好! config.setConnectionTimeout(30000); config.setIdleTimeout(600000);
-
终极武器:链路追踪
graph LR A[用户请求] --> B[API网关] B --> C[订单服务] C --> D[MySQL数据库] D -->|慢查询| E[定位具体SQL] E --> F[EXPLAIN分析] F --> G[索引优化]
七、面试考点:吊打面试官的秘籍
高频考题
-
Q:如何优化
SELECT COUNT(*)
?A:分场景!MyISAM直接返回元数据,InnoDB用二级索引或Redis计数
-
Q:EXPLAIN中Using filesort是什么意思?
A:表示额外排序,可能需优化索引或调整
sort_buffer_size
-
Q:为什么索引加快了查询却拖慢了写入?
A:索引本质是"空间换时间",每次INSERT需更新所有相关索引(B+树平衡代价)
-
Q:如何强制使用某个索引?
A:
SELECT /*+ INDEX(table_name idx_name) */ ...
,但需谨慎!
超纲加分题
sql
-- 问题:这条SQL有什么问题?
SELECT * FROM users
WHERE date(create_time) = '2025-06-30';
-- 答案解析:
-- 1. 对索引列使用函数导致索引失效
-- 2. 正确写法:create_time BETWEEN '2025-06-30 00:00:00' AND '2025-06-30 23:59:59'
八、总结:慢查询优化降龙十八掌
- 监控先行:没有度量就没有优化
- 日志为证:慢查询日志是犯罪现场记录
- 执行计划是地图:EXPLAIN指引优化方向
- 索引是双刃剑:精准创建,避免过度
- SQL需解剖 :用
SHOW PROFILE
分析执行细节 - 架构补充:读写分离/缓存分担查询压力
优化永无止境!记住数据库大师的教诲:"最快的查询是不发生的查询。" 当你征服了慢查询的崇山峻岭,回头望去------那曾经让你夜不能寐的15秒延迟,不过是性能海洋中的一朵小浪花罢了。