MySQL查询速度慢怎么办?慢查询日志、EXPLAIN与索引优化教程

MySQL查询速度慢怎么办?慢查询日志、EXPLAIN与索引优化教程

MySQL查询变慢时,最容易犯的错误是先调参数、加内存,或者看到某个字段就立刻创建索引。这样做偶尔有效,但更多时候只是掩盖问题,甚至会增加写入开销。

比较稳妥的排查顺序是:先确认慢在数据库还是应用链路,再用慢查询日志或Performance Schema找出高消耗SQL,接着分析执行计划,最后才决定改SQL、加索引还是调整数据结构。

本文以MySQL 8为主,介绍一套可以在生产环境中实际使用的慢查询排查方法。

一、先确认“慢”发生在哪里

页面响应慢,不一定等于SQL执行慢。连接池等待、网络延迟、锁等待、磁盘I/O、CPU争用和应用代码都可能拉长总耗时。

先查看MySQL当前是否存在明显压力:

SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Threads_connected',
  'Threads_running',
  'Questions',
  'Slow_queries',
  'Created_tmp_tables',
  'Created_tmp_disk_tables'
);

再查看当前会话:

SHOW FULL PROCESSLIST;

重点关注以下情况:

  • 同一条查询同时运行很多次。
  • 查询长时间处于Sending dataCreating sort index或临时表相关状态。
  • 大量会话等待元数据锁、行锁或表锁。
  • Threads_running持续偏高,而不是只在某个瞬间升高。

SHOW PROCESSLIST只能看到当前现场。如果慢SQL执行几百毫秒后就结束,人工查看时很容易错过,因此还需要慢查询日志或Performance Schema做持续统计。

二、检查并启用慢查询日志

先查看当前配置:

SHOW VARIABLES
WHERE Variable_name IN (
  'slow_query_log',
  'slow_query_log_file',
  'long_query_time',
  'min_examined_row_limit',
  'log_queries_not_using_indexes',
  'log_output'
);

常用参数含义如下:

参数作用
slow_query_log是否启用慢查询日志
slow_query_log_file慢查询日志文件路径
long_query_time查询执行时间超过该值时记录,支持微秒精度
min_examined_row_limit扫描行数达到设定值后才记录
log_queries_not_using_indexes是否额外记录未使用索引的查询
log_output输出到文件、表或两者

临时启用并把阈值设为1秒:

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;

修改全局变量只影响之后建立的新会话,已经存在的会话可能继续使用原来的long_query_time。如果要让设置在MySQL重启后仍然生效,应写入MySQL配置文件,或在确认版本和权限支持后使用SET PERSIST

不要一开始就把阈值降到极低,也不要长期无条件开启log_queries_not_using_indexes。小阈值和高并发叠加时会产生大量日志;未使用索引也不等于查询一定有问题,例如扫描很小的表可能比走索引更快。

快速汇总慢查询日志

MySQL自带的mysqldumpslow可以对日志中的相似SQL进行归类:

mysqldumpslow -s t -t 20 /path/to/mysql-slow.log

这条命令按查询时间排序,显示前20组SQL。还可以使用:

mysqldumpslow -s c -t 20 /path/to/mysql-slow.log
mysqldumpslow -s at -t 20 /path/to/mysql-slow.log
  • -s c:按执行次数排序。
  • -s at:按平均查询时间排序。

不要只盯着“最慢的一次”。一条平均耗时200毫秒、每天执行几十万次的SQL,可能比偶尔运行一次的10秒报表查询更值得优先处理。

三、用Performance Schema找高消耗SQL

MySQL 8通常可以从Performance Schema的语句摘要表中查看聚合结果:

SELECT
  DIGEST_TEXT,
  COUNT_STAR,
  ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_seconds,
  ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms,
  SUM_ROWS_EXAMINED,
  SUM_ROWS_SENT,
  SUM_CREATED_TMP_DISK_TABLES,
  SUM_SORT_ROWS
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;

这类统计更适合发现“单次不算特别慢,但调用频繁、累计消耗很高”的SQL。排查时至少同时看四个维度:

  • 总耗时:决定它对数据库总负载的贡献。
  • 平均耗时:反映单次请求体验。
  • 执行次数:判断是否存在异常频率或重复查询。
  • 扫描行数与返回行数:两者差距很大时,通常值得检查过滤条件和索引。

Performance Schema的统计从MySQL实例启动或摘要表上次清空后开始累计。不要把很长时间的历史总量直接当作当前故障现场,最好结合监控时间窗口判断。

四、用EXPLAIN看执行计划

找到目标SQL后,先使用EXPLAIN

EXPLAIN
SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 10086
  AND status = 'paid'
  AND created_at >= '2026-08-01'
ORDER BY created_at DESC
LIMIT 20;

常见字段可以这样理解:

字段排查重点
type访问方式。出现ALL通常表示全表扫描,但小表全扫不一定有问题
possible_keys优化器认为可能使用的索引
key实际选择的索引
rows预计需要检查的行数,不是精确实测值
filtered经过条件过滤后预计保留的百分比
Extra额外操作,例如Using temporaryUsing filesort

Using filesort不等于一定使用磁盘,也不代表查询必然很慢;它表示排序没有直接按索引顺序完成。Using temporary同样需要结合数据量、执行频率和实际耗时判断,不能只看到关键词就下结论。

EXPLAIN ANALYZE要谨慎使用

MySQL 8.0.18及更高版本支持EXPLAIN ANALYZE

EXPLAIN ANALYZE
SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 10086
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

它会真正执行语句,并返回估算成本、实际耗时、实际行数和循环次数,因此比普通EXPLAIN更容易发现估算偏差。

也正因为它会执行SQL,不要在生产高峰直接分析未知成本的大查询。对UPDATEDELETE等写操作尤其要谨慎,应先在测试环境、只读副本或经过严格限制的条件下验证。

五、根据查询方式设计联合索引

以上面的订单查询为例,可以考虑:

CREATE INDEX idx_orders_user_status_created
ON orders (user_id, status, created_at);

这个索引先按user_idstatus做等值定位,再利用created_at处理时间范围和排序。它是否合适,仍然取决于字段基数、数据分布、查询比例和其他业务SQL。

联合索引遵循最左前缀原则。索引(user_id, status, created_at)通常可以支持以user_id开头的查询,但不能简单认为只按statuscreated_at查询也能高效使用整个索引。

创建索引前先检查现有索引:

SHOW INDEX FROM orders;

避免创建功能高度重叠的索引。索引不是越多越好,每个额外索引都会占用磁盘和缓冲池,并增加INSERTUPDATEDELETE的维护成本。

六、常见的索引失效或低效写法

1. 对索引列做函数计算

不推荐:

SELECT * FROM orders
WHERE DATE(created_at) = '2026-08-19';

可改为范围查询:

SELECT * FROM orders
WHERE created_at >= '2026-08-19 00:00:00'
  AND created_at <  '2026-08-20 00:00:00';

2. 字段类型与查询参数不一致

如果手机号字段是字符串,却使用数字参数比较,可能发生隐式类型转换。应用层应按列的真实类型传参,并通过执行计划确认索引是否被使用。

3. LIKE以通配符开头

WHERE title LIKE '%mysql%'

普通B-tree索引通常无法利用已知前缀快速定位。大量文本检索应考虑FULLTEXT索引或专用搜索服务,而不是不断叠加普通索引。

4. 深分页

SELECT id, title
FROM articles
ORDER BY id
LIMIT 500000, 20;

偏移量很大时,数据库仍要扫描并跳过前面的记录。可以改用基于上一页最后一条记录的游标分页:

SELECT id, title
FROM articles
WHERE id > 500000
ORDER BY id
LIMIT 20;

5. 返回不需要的列

SELECT *会增加数据读取、网络传输和回表成本,也会让覆盖索引更难实现。接口只查询实际需要的字段,通常更容易控制性能。

七、索引存在,MySQL为什么仍然不用

优化器会根据统计信息和成本选择执行计划。索引存在但未被使用,常见原因包括:

  • 表很小,全表扫描成本更低。
  • 条件会命中表中很大比例的数据,索引选择性不足。
  • 统计信息过旧,优化器对行数估算偏差较大。
  • 查询条件、排序方式与索引列顺序不匹配。
  • 隐式类型转换或函数运算改变了访问方式。

可以更新表统计信息:

ANALYZE TABLE orders;

这不是日常“加速按钮”。执行前应评估表大小、版本和业务影响,并在执行后重新检查计划是否真的改善。

不要长期依赖FORCE INDEX掩盖问题。数据分布变化后,被强制指定的索引可能从优势变成负担。索引提示更适合作为经过验证的临时手段,而不是替代索引和SQL设计。

八、修改后怎样确认优化有效

完成优化后,至少做以下检查:

  1. 对比修改前后的EXPLAINEXPLAIN ANALYZE结果。
  2. 检查扫描行数、返回行数、排序和临时表是否减少。
  3. 在接近真实数据量和并发的环境中压测。
  4. 观察应用接口的平均耗时和P95、P99延迟。
  5. 观察CPU、磁盘I/O、缓冲池命中和锁等待是否出现副作用。
  6. 检查新增索引对写入速度、磁盘空间和备份时间的影响。

新增大表索引可能持续较长时间,并占用CPU、I/O和临时空间。即使版本支持在线DDL,也不代表完全没有业务影响。生产环境应提前确认磁盘余量、变更窗口、回滚方案和复制延迟。

九、一套实用的排查顺序

遇到MySQL查询慢,可以按下面的顺序处理:

  1. 从应用监控确认慢接口、发生时间和调用量。
  2. 查看SHOW FULL PROCESSLIST及锁等待,保存故障现场。
  3. 使用慢查询日志或Performance Schema筛选高消耗SQL。
  4. 先用EXPLAIN检查访问方式、索引、估算行数和额外操作。
  5. 在可控环境使用EXPLAIN ANALYZE核对实际执行情况。
  6. 根据过滤、关联、排序和分页方式调整SQL或联合索引。
  7. 用真实数据验证,并持续观察读写两侧的变化。

真正有效的优化,不是让执行计划看起来更漂亮,而是在业务相同、结果正确的前提下,稳定减少数据库需要处理的数据和工作量。

知识库

网站出现重定向次数过多怎么办?Nginx、HTTPS与CDN循环跳转排查教程

2026-8-18 18:00:54

知识库

Docker容器反复重启怎么办?退出码、日志与健康检查排查教程

2026-8-19 18:11:16