服务器与大带宽专家 · 持牌IDC/CDN/ISP服务商
简米科技官网JIANMI TECH
资讯 2026-09-03 更新于 2026-09-03 简米科技 5,448 字 13 分钟阅读

数据库慢查询频发时从索引到SQL的逐步排查路径?,慢查询优化方法有哪些

导读数据库慢查询频发时,正确的排查路径是:先开启慢查询日志抓取具体SQL,再用EXPLAIN检查执行计划,重点看索引是否生效,最后通过改写SQL或调整索引来消除性能瓶颈, 很多人遇到这个问题,第一反应是“加索引”,结果加了还是慢,因为慢查询的根因往往不只是缺少索引,还可能是索引设计不合理或SQL写法让索引失效,下面……

数据库慢查询频发时,正确的排查路径是:先开启慢查询日志抓取具体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列,它从好到差依次是systemconsteq_refrefrangeindexALL,看到ALL基本就实锤了全表扫描。

下面这张表可以帮你快速判断当前的健康程度:

type值 含义 是否需要优化
ALL 全表扫描,扫描行数等于全表行数 必须优化
index 扫描了整棵索引树 看情况
range 索引范围扫描,比如用了>、<、BETWEEN

数据库慢查询频发时从索引到SQL的逐步排查路径?,慢查询优化方法有哪些

可接受

ref 非唯一索引等值匹配 良好
eq_ref 唯一索引等值匹配,通常出现在JOIN中 优秀
const 主键或唯一索引等值匹配 极佳

另外注意Extra列,如果出现Using filesort,意味着ORDER BY字段没有索引支撑,MySQL要额外做一次文件排序,这也是慢查询的常见来源,如果出现Using temporary,说明用了临时表,通常和GROUP BY或DISTINCT有关。

索引失效原因排查:为什么建了索引却还是慢

有时候EXPLAIN的结果显示key不是NULL,但扫描行数依然巨大,或者typeALL,这说明索引建了,但没被正确使用,以下是四个高频原因,每一个都有对应的修复动作。

隐式类型转换:你的索引被“隐形”了

假设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开头才能命中这个索引,直接查statuscreate_time,索引便形同虚设。

比如下面这条SQL:

SELECT  FROM orders WHERE status = 1 ORDER BY create_time DESC;

尽管索引里有status列,但缺少了user_id,MySQL不会使用这个联合索引,业内专家指出,设计联合索引时,要把等值查询的字段放在最前面,范围查询的字段放在后面,排序字段最好也包含进来,这样既能过滤又能排序。

统计信息过期:让优化器做出“错误决策”

MySQL优化器依赖表的统计信息来决定是否走索引,如果表数据频繁增删,统计信息没更新,优化器可能觉得“走索引还不如全表扫描快”,于是主动放弃索引。

数据库慢查询频发时从索引到SQL的逐步排查路径?,慢查询优化方法有哪些

这种情况用一条命令就能解决:

ANALYZE TABLE orders;

执行后更新统计信息,很多DBA在跑完大批量数据变更后,会顺手执行这条命令,避免后续查询走偏。

SQL改写实操:不伤业务的性能提升手段

排查完索引问题,接下来就是对SQL本身做瘦身,许多慢查询并非索引缺失,而是查询方式太“重”,带回了大量不需要的数据。

避免SELECT ,只取必要字段

SELECT 会把所有列都查出来,包括大字段TEXTBLOB,这一方面增加了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_idstatus,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跑一段时间,再查看日志,如果这条SQL不再出现,说明已经降到阈值以下,如果想更精确,可以手动执行并开启profiling

SET profiling = 1;
SELECT  FROM orders WHERE user_id = 12345 ORDER BY create_time DESC;
SHOW PROFILES;

执行SHOW PROFILES后,你会看到每个语句的耗时明细,包含Sending dataSorting result等阶段的时间分布,如果Sorting result占比很高,说明还需要调整排序相关的索引。

用并发压测确认线上稳定性

单次执行快不代表并发下就稳,用sysbenchmysqlslap模拟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,应该先优化哪个?
先看EXPLAINrows列和Extra列。rows越大说明扫描越严重,优先处理type=ALLUsing filesort的SQL,另外按照慢查询日志中的总耗时排序,总耗时 = 平均执行时间 × 执行次数,执行次数多的低延迟查询优化收益往往更大。

问题3:加了索引后,为什么写入变慢了?
索引不是免费的,每次INSERTUPDATEDELETE都需要额外维护索引树,如果业务是写多读少,索引过多会让写入压力成倍增加,比较合理的做法是保留热度最高的两到三个索引,把低频查询改到从库上执行,用读写分离换取写性能,优化无止境,但每次改动都应基于慢查询日志和EXPLAIN的结果,而不是主观猜测,走完“抓日志 - 看计划 - 查索引 - 改SQL - 验效果”这条路径,大多数数据库慢查询都能被有效遏制。

分享本文
本文为 简米科技官网 原创,已由运维技术专家审核。转载请注明来源:原文链接
售前咨询 服务热线 售后 邮箱