数据库慢查询频发时,正确的排查路径是:先开启慢查询日志抓取具体SQL,再用EXPLAIN检查执行计划,重点看索引是否生效,最后通过改写SQL或调整索引来消除性能瓶颈。 很多人遇到这个问题,第一反应是“加索引”,结果加了还是慢,因为慢查询的根因往往不只是缺少索引,还可能是索引设计不合理或SQL写法让索引失效,下面这条路径,可以帮你少走弯路。
慢查询优化步骤:从定位到分析的标准动作
排查慢查询,最忌讳凭感觉猜,数据库本身已经把线索记录在了慢查询日志里,你需要做的第一步是让它开口告诉你“谁拖了我的后腿”。
开启慢查询日志,让数据库自己汇报问题
MySQL的慢查询日志默认是关闭的,需要手动打开,在配置文件my.cnf的[mysqld]区域加入以下参数:
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = ON
long_query_time表示超过多少秒算慢查询,业务压力大的库建议设为1秒,压力小的可以设为0.5秒,设置完成后重启MySQL,或者用SET GLOBAL动态开启:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
在电商订单场景里,如果用户查历史订单时经常转圈,慢查询日志就会把那条耗时超过1秒的SELECT语句完整记录下来,包括执行时间、锁等待时间、返回行数,有了这些信息,你就知道该盯哪条SQL了。
常用的分析工具是mysqldumpslow,它能把日志里的SQL按执行次数或总耗时排序。
mysqldumpslow -t 10 /var/log/mysql/slow.log
这条命令返回前10条最耗时的SQL,你会看到类似SELECT FROM orders WHERE user_id = N ORDER BY create_time DESC这样的语句,带着N这类参数占位符,方便你快速识别出重复出现的模式。
用EXPLAIN查看执行计划,直击索引使用真相
拿到具体SQL后,别急着改代码,先在这个SQL前面加上EXPLAIN,让MySQL告诉你它会怎么执行:
EXPLAIN SELECT FROM orders WHERE user_id = 12345 ORDER BY create_time DESC;
返回结果里有一个key列,显示MySQL实际使用的索引,如果这个值是NULL,说明没走索引,正在全表扫描,还有一个type列,它从好到差依次是system、const、eq_ref、ref、range、index、ALL,看到ALL基本就实锤了全表扫描。
下面这张表可以帮你快速判断当前的健康程度:
| type值 | 含义 | 是否需要优化 |
|---|---|---|
| ALL | 全表扫描,扫描行数等于全表行数 | 必须优化 |
| index | 扫描了整棵索引树 | 看情况 |
| range | 索引范围扫描,比如用了>、<、BETWEEN |
可接受 |
| ref | 非唯一索引等值匹配 | 良好 |
| eq_ref | 唯一索引等值匹配,通常出现在JOIN中 | 优秀 |
| const | 主键或唯一索引等值匹配 | 极佳 |
另外注意Extra列,如果出现Using filesort,意味着ORDER BY字段没有索引支撑,MySQL要额外做一次文件排序,这也是慢查询的常见来源,如果出现Using temporary,说明用了临时表,通常和GROUP BY或DISTINCT有关。
索引失效原因排查:为什么建了索引却还是慢
有时候EXPLAIN的结果显示key不是NULL,但扫描行数依然巨大,或者type是ALL,这说明索引建了,但没被正确使用,以下是四个高频原因,每一个都有对应的修复动作。
隐式类型转换:你的索引被“隐形”了
假设phone字段在表里是VARCHAR类型,但查询时用了数字:
SELECT FROM users WHERE phone = 13800138000;
MySQL会把字段类型转换成数字再做比较,这个转换过程会导致索引失效,这是“看不见”的破坏,很多老系统踩过这个坑,行业共识认为,定期巡检线上SQL时,要特别留意字段类型和查询参数是否完全匹配。
修复方法很简单:把查询条件改成字符串形式,或者统一应用层传参类型:
SELECT FROM users WHERE phone = '13800138000';
函数操作:索引在计算面前“罢工”
对索引字段做函数运算,优化器会放弃使用索引,比如统计某一天的新增用户:
SELECT FROM users WHERE DATE(create_time) = '2026-03-15';
create_time上即使有索引,遇到DATE()函数也只能全表扫,正确的做法是把它改写成范围查询:
SELECT FROM users
WHERE create_time >= '2026-03-15 00:00:00'
AND create_time < '2026-03-16 00:00:00';
这样就能利用上create_time上的普通索引,type变成range,效率提升一个量级。
联合索引最左前缀原则:顺序不对全白费
如果你的表有一个联合索引(user_id, status, create_time),那么查询条件必须以user_id开头才能命中这个索引,直接查status或create_time,索引便形同虚设。
比如下面这条SQL:
SELECT FROM orders WHERE status = 1 ORDER BY create_time DESC;
尽管索引里有status列,但缺少了user_id,MySQL不会使用这个联合索引,业内专家指出,设计联合索引时,要把等值查询的字段放在最前面,范围查询的字段放在后面,排序字段最好也包含进来,这样既能过滤又能排序。
统计信息过期:让优化器做出“错误决策”
MySQL优化器依赖表的统计信息来决定是否走索引,如果表数据频繁增删,统计信息没更新,优化器可能觉得“走索引还不如全表扫描快”,于是主动放弃索引。

这种情况用一条命令就能解决:
ANALYZE TABLE orders;
执行后更新统计信息,很多DBA在跑完大批量数据变更后,会顺手执行这条命令,避免后续查询走偏。
SQL改写实操:不伤业务的性能提升手段
排查完索引问题,接下来就是对SQL本身做瘦身,许多慢查询并非索引缺失,而是查询方式太“重”,带回了大量不需要的数据。
避免SELECT ,只取必要字段
SELECT 会把所有列都查出来,包括大字段TEXT或BLOB,这一方面增加了IO开销,另一方面也可能迫使临时表使用磁盘而非内存,改成只查询需要的列,
SELECT order_id, amount, status FROM orders WHERE user_id = 12345;
这样不仅扫描的数据量变小,配合覆盖索引还能避免回表。
把大IN拆成小批,或者改成JOIN
一条SQL里IN后面挂了几千个ID,优化器往往会选择全表扫描,因为统计信息无法准确估算这么多值的分布,大部分情况下,把大IN拆成多个小批(每批几百个)执行,总耗时反而更低,但更推荐的做法是,将ID列表放到一个临时表里,然后与目标表做JOIN:
-- 先建临时表 tmp_ids
SELECT o. FROM orders o
INNER JOIN tmp_ids t ON o.id = t.id;
这样能让优化器更精确地估算行数,走索引的概率大大增加。
用覆盖索引消除回表,让Extra显示Using index
当查询的所有字段都包含在索引中时,MySQL无需回表读取数据行,直接扫描索引树就能得到结果,比如你有一个索引(user_id, status),执行:
SELECT user_id, status FROM orders WHERE status = 1;
虽然status不是最左前缀,但如果查询字段只有user_id和status,MySQL可能选择全索引扫描(type=index),至少比全表扫描快,因为索引树比数据行小得多,更典型的场景是设计覆盖索引来支持高频查询:
ALTER TABLE orders ADD INDEX idx_user_status_create (user_id, status, create_time);
然后查询:
SELECT id, create_time FROM orders
WHERE user_id = 12345 AND status = 1;
此时Extra列会显示Using index,意味着整个查询在索引内完成,回表和文件排序都被省掉了。
分页深度优化:延迟关联替代limit大偏移
经典的分页场景:
SELECT FROM orders ORDER BY create_time DESC LIMIT 100000, 20;
MySQL需要先扫描前100000行,再丢弃它们,代价极高,延迟关联的做法是先从索引上定位出起始ID,再回表取数据:
SELECT FROM orders
INNER JOIN (
SELECT id FROM orders
ORDER BY create_time DESC
LIMIT 100000, 20
) AS tmp ON orders.id = tmp.id;
子查询只查id,走覆盖索引,回表次数被压缩到20次,这种方法在后台管理系统的翻页列表中非常实用。
验证与回归:让慢查询优化效果可量化

改完SQL和索引后,不能拍拍手就结束,你需要用数据证明“确实变快了”,否则下一次变更可能引入新的问题。
再次对比慢查询日志,观察执行时间和扫描行数
把慢查询日志清空或记住当前行数,然后让优化后的SQL跑一段时间,再查看日志,如果这条SQL不再出现,说明已经降到阈值以下,如果想更精确,可以手动执行并开启profiling:
SET profiling = 1;
SELECT FROM orders WHERE user_id = 12345 ORDER BY create_time DESC;
SHOW PROFILES;
执行SHOW PROFILES后,你会看到每个语句的耗时明细,包含Sending data、Sorting result等阶段的时间分布,如果Sorting result占比很高,说明还需要调整排序相关的索引。
用并发压测确认线上稳定性
单次执行快不代表并发下就稳,用sysbench或mysqlslap模拟20到50个并发线程,执行优化后的SQL,重点观察两个指标:平均响应时间和每秒查询数(QPS),如果优化前响应时间是2秒,优化后变成0.3秒,QPS从200升到800,那这次优化就是成功的。
关注缓存与锁竞争
有些场景下,SQL已经没问题,但线上依然慢,此时要检查InnoDB_buffer_pool_size是否过小,导致数据频繁从磁盘读取,另外查看SHOW ENGINE INNODB STATUS,看有没有大量锁等待,这一层属于更深入的系统级调优,索引和SQL优化之后再排查不迟。
线上数据库慢查询怎么排查?三个高频问题解答
问题1:慢查询日志文件越来越大,占满磁盘怎么办?
定期分析后备份清理,用mysqldumpslow提取出Top N SQL并处理完问题后,可以执行SET GLOBAL slow_query_log = OFF;然后删除或清空日志文件,再重新开启,也可以用pt-query-digest工具生成报表,同时让日志按天轮转,避免单文件无限膨胀。
问题2:同一张表上,多个慢查询SQL,应该先优化哪个?
先看EXPLAIN中rows列和Extra列。rows越大说明扫描越严重,优先处理type=ALL和Using filesort的SQL,另外按照慢查询日志中的总耗时排序,总耗时 = 平均执行时间 × 执行次数,执行次数多的低延迟查询优化收益往往更大。
问题3:加了索引后,为什么写入变慢了?
索引不是免费的,每次INSERT、UPDATE、DELETE都需要额外维护索引树,如果业务是写多读少,索引过多会让写入压力成倍增加,比较合理的做法是保留热度最高的两到三个索引,把低频查询改到从库上执行,用读写分离换取写性能,优化无止境,但每次改动都应基于慢查询日志和EXPLAIN的结果,而不是主观猜测,走完“抓日志 - 看计划 - 查索引 - 改SQL - 验效果”这条路径,大多数数据库慢查询都能被有效遏制。
