【Java项目技术亮点】EXPLAIN深度分析与慢查询治理
2026/7/5 2:07:10 网站建设 项目流程

写在前面:说实话,做后端开发这些年,我见过太多项目上线后因为一条慢SQL把数据库拖垮的事故。有一次凌晨两点被叫醒,生产环境MySQL CPU飙到90%,一查发现一条统计SQL执行了30秒,连接池被打满,整个服务差点雪崩。那次之后我才真正重视起慢查询治理。这篇文章把我踩过的坑和积累的经验整理出来,希望能帮你少走点弯路。

文章目录

    • 一、为什么慢查询是性能杀手?
      • 1.1 一个真实的生产事故
      • 1.2 慢查询的危害
      • 1.3 生活类比:堵车
    • 二、开启慢查询日志
      • 2.1 MySQL配置
      • 2.2 查看慢查询日志
      • 2.3 慢查询日志分析要点
    • 三、EXPLAIN字段逐行解读
      • 3.1 EXPLAIN基本用法
      • 3.2 id列:执行顺序
      • 3.3 select_type列:查询类型
      • 3.4 table列:访问的表
      • 3.5 type列:访问类型(重点!)
      • 3.6 possible_keys列:可能使用的索引
      • 3.7 key列:实际使用的索引
      • 3.8 key_len列:索引使用长度
      • 3.9 ref列:索引匹配条件
      • 3.10 rows列:预估扫描行数
      • 3.11 Extra列:额外信息(重点!)
    • 四、常见慢查询场景与优化
      • 4.1 场景1:全表扫描(type=ALL)
      • 4.2 场景2:索引失效
      • 4.3 场景3:Using filesort(ORDER BY无索引)
      • 4.4 场景4:Using temporary(GROUP BY/DISTINCT无索引)
      • 4.5 场景5:大表JOIN效率低
    • 五、慢查询治理体系
      • 5.1 事前:SQL Review
      • 5.2 事中:实时监控
      • 5.3 事后:定期分析
      • 5.4 治理流程
    • 六、踩坑指南
    • 七、问题与解答
      • Q1:EXPLAIN和EXPLAIN ANALYZE有什么区别?
      • Q2:为什么加了索引,查询还是很慢?
      • Q3:慢查询日志记录了太多数据,怎么过滤?
    • 八、面试高频考点汇总
      • 考点1:EXPLAIN中type列有哪些值?性能排序是怎样的?
      • 考点2:Extra列中Using filesort和Using temporary代表什么?
      • 考点3:什么是覆盖索引?
      • 考点4:索引失效的常见场景有哪些?
      • 考点5:如何分析一条慢SQL?
    • 九、模拟面试官提问和参考答案
      • 场景题1:生产环境CPU飙高,你如何判断是不是慢查询导致的?
      • 场景题2:有个分页查询 `LIMIT 1000000, 10` 很慢,怎么优化?
      • 场景题3:表有联合索引 (a, b, c),以下SQL能否用到索引?
      • 场景题4:索引加了,但EXPLAIN显示不走索引,可能是什么原因?
      • 场景题5:如何设计一套慢查询治理方案?
    • 十、互动话题
    • 十一、参考资料

一、为什么慢查询是性能杀手?

1.1 一个真实的生产事故

去年双十一前夕,我们系统的订单查询接口突然超时。

监控告警狂响,MySQL CPU飙到90%,QPS从平时的2000暴跌到200。

紧急排查发现,运营同学跑了一条统计SQL:

SELECTCOUNT(*)FROMorderWHEREcreate_time>'2024-01-01'ANDstatus=1;

这条SQL执行了30秒,扫描了800万行数据。

连接池被打满,其他正常请求全部排队等待,整个系统差点雪崩。

1.2 慢查询的危害

危害类型具体表现影响程度
响应延迟接口响应从100ms变成10s+用户体验极差
连接池打满数据库连接被慢查询占满系统不可用
雪崩效应上游服务超时重试,流量翻倍级联故障
主从延迟慢查询在主库执行,从库复制滞后数据不一致

1.3 生活类比:堵车

慢查询就像城市主干道上的严重堵车。

一条主干道堵了,整个城市的交通都受影响。

救护车、消防车过不去,后果可想而知。

数据库也是一样,一条慢SQL能把整个系统拖下水。


二、开启慢查询日志

2.1 MySQL配置

找到MySQL配置文件(通常是my.cnfmy.ini),添加或修改以下配置:

[mysqld] # 开启慢查询日志 slow_query_log = 1 # 慢查询日志文件路径 slow_query_log_file = /var/log/mysql/slow.log # 超过1秒的查询记录为慢查询 long_query_time = 1 # 记录未使用索引的查询(建议开启) log_queries_not_using_indexes = 1

修改后重启MySQL:

sudosystemctl restart mysql

2.2 查看慢查询日志

方式一:直接查看日志文件

# 查看最新的慢查询tail-f/var/log/mysql/slow.log

方式二:使用 mysqldumpslow 工具

# 按执行时间排序,显示前10条mysqldumpslow-st-t10/var/log/mysql/slow.log# 按执行次数排序mysqldumpslow-sc-t10/var/log/mysql/slow.log

方式三:使用 pt-query-digest 工具(推荐)

# 安装 Percona Toolkitsudoapt-getinstallpercona-toolkit# 分析慢查询日志,生成详细报告pt-query-digest /var/log/mysql/slow.log>slow_query_report.txt

pt-query-digest输出包含:

  • Rank:按查询时间占比排名
  • Query ID:查询指纹
  • Response time:总响应时间和单次平均时间
  • Calls:执行次数
  • R/Call:每次调用平均时间
  • V/M:响应时间方差均值(越大越不稳定)

2.3 慢查询日志分析要点

指标含义关注重点
频率多久出现一次高频慢查询优先处理
执行时间单次执行多久超过1秒就要警惕
返回行数返回了多少数据返回行数远大于需要 = 浪费
扫描行数扫描了多少行扫描行数 / 返回行数 > 100 = 严重

三、EXPLAIN字段逐行解读

3.1 EXPLAIN基本用法

EXPLAINSELECT*FROMuserWHEREid=100;

输出大概长这样:

+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | 1 | SIMPLE | user | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+

下面逐字段解读。

3.2 id列:执行顺序

id相同,执行顺序从上到下;id不同,id越大越先执行。

-- id相同的情况EXPLAINSELECT*FROMuseru,orderoWHEREu.id=o.user_id;-- id都是1,先执行user表,再执行order表-- id不同的情况(子查询)EXPLAINSELECT*FROMuserWHEREid=(SELECTuser_idFROMorderWHEREid=100);-- 子查询id=2先执行,外层id=1后执行

3.3 select_type列:查询类型

类型含义示例
SIMPLE简单查询,不包含子查询或UNIONSELECT * FROM user
PRIMARY最外层查询包含子查询时的外层
SUBQUERY子查询WHERE id IN (SELECT ...)
DERIVED派生表(FROM后的子查询)FROM (SELECT ...) AS t
UNIONUNION中的第二个及后续查询SELECT ... UNION SELECT ...

3.4 table列:访问的表

显示当前行访问的是哪张表。

如果是派生表,会显示<derivedN>,N是子查询的id。

3.5 type列:访问类型(重点!)

type是判断SQL性能的核心指标,性能从好到差:

system > const > eq_ref > ref > range > index > ALL
type含义性能示例
system表只有一行数据极好系统表
const通过主键或唯一索引一次命中极好WHERE id = 1
eq_refJOIN时,被驱动表通过主键或唯一索引匹配极好联表查询,ON条件是主键
ref通过普通索引匹配WHERE name = '张三'(name有索引)
range索引范围扫描还行WHERE id BETWEEN 1 AND 100
index全索引扫描较差扫描整个索引树
ALL全表扫描极差没有用到索引

踩坑提醒:type出现indexALL就要警惕了。我见过太多人看到index以为用了索引就万事大吉,实际上index是扫描整个索引树,和全表扫描差不了多少!

3.6 possible_keys列:可能使用的索引

显示MySQL认为可能用到的索引。

注意:只是"可能",实际不一定用。

3.7 key列:实际使用的索引

显示MySQL实际选择的索引。

如果为NULL,表示没有用到索引。

3.8 key_len列:索引使用长度

这个字段很关键!可以判断联合索引实际用了几个字段。

数据类型key_len
int4
bigint8
varchar(20)20 * 4 + 2 = 82(utf8mb4)
datetime5

示例:

-- 联合索引 idx_name_age (name, age)EXPLAINSELECT*FROMuserWHEREname='张三'ANDage=20;-- key_len = 82 + 4 = 86,说明两个字段都用到了EXPLAINSELECT*FROMuserWHEREname='张三';-- key_len = 82,说明只用到了name字段

3.9 ref列:索引匹配条件

显示索引的哪一列被使用了,常见值:

  • const:常量匹配
  • 库名.表名.字段名:表的字段匹配

3.10 rows列:预估扫描行数

MySQL预估需要扫描的行数。这个数字越小越好

踩坑提醒:rows是预估的,不是实际的!基于统计信息计算,如果统计信息过期,这个值可能偏差很大。我踩过这个坑,EXPLAIN显示rows=100,实际执行扫描了100万行。

3.11 Extra列:额外信息(重点!)

Extra值含义好坏
Using index覆盖索引,不需要回表极好
Using index conditionICP索引下推,减少回表
Using whereServer层过滤数据一般
Using filesort文件排序,没有用到索引排序
Using temporary使用了临时表
Using join buffer使用Join缓存一般

示例解读:

-- 覆盖索引,性能好EXPLAINSELECTid,nameFROMuserWHEREname='张三';-- Extra: Using index-- 文件排序,性能差EXPLAINSELECT*FROMuserORDERBYage;-- Extra: Using filesort-- 使用了临时表EXPLAINSELECTstatus,COUNT(*)FROMuserGROUPBYstatus;-- Extra: Using temporary; Using filesort

四、常见慢查询场景与优化

4.1 场景1:全表扫描(type=ALL)

问题SQL:

-- user表的phone字段没有索引EXPLAINSELECT*FROMuserWHEREphone='13800138000';

EXPLAIN结果:

type: ALL rows: 1000000 Extra: Using where

优化方案:加索引

-- 添加索引ALTERTABLEuserADDINDEXidx_phone(phone);-- 再次EXPLAINtype: refrows:1key: idx_phone

4.2 场景2:索引失效

2.1 隐式类型转换

-- phone是varchar类型,传入数字EXPLAINSELECT*FROMuserWHEREphone=13800138000;-- type: ALL,索引失效!-- 正确写法EXPLAINSELECT*FROMuserWHEREphone='13800138000';-- type: ref,索引生效

2.2 对索引字段做函数操作

-- 错误:对create_time做函数操作EXPLAINSELECT*FROMuserWHEREYEAR(create_time)=2024;-- type: ALL-- 正确:改写为范围查询EXPLAINSELECT*FROMuserWHEREcreate_timeBETWEEN'2024-01-01'AND'2024-12-31';-- type: range

2.3 like ‘%xxx’ 前缀模糊

-- 前缀模糊,索引失效EXPLAINSELECT*FROMuserWHEREnameLIKE'%张三%';-- type: ALL-- 后缀模糊,索引生效EXPLAINSELECT*FROMuserWHEREnameLIKE'张三%';-- type: range

4.3 场景3:Using filesort(ORDER BY无索引)

问题SQL:

-- 按create_time排序,但create_time没有索引EXPLAINSELECT*FROMorderWHEREuser_id=100ORDERBYcreate_timeDESCLIMIT10;-- Extra: Using where; Using filesort

优化方案:加复合索引

-- 添加复合索引(注意字段顺序!)ALTERTABLE`order`ADDINDEXidx_user_time(user_id,create_time);-- 再次EXPLAIN-- Extra: Using index condition-- 索引直接有序,不需要额外排序

踩坑提醒:复合索引的字段顺序很重要!把等值查询的字段放前面,范围查询/排序的字段放后面。我见过太多人索引字段顺序写反了,结果索引只用了一半。

4.4 场景4:Using temporary(GROUP BY/DISTINCT无索引)

问题SQL:

EXPLAINSELECTstatus,COUNT(*)FROMuserGROUPBYstatus;-- Extra: Using temporary; Using filesort

优化方案:

-- 方案1:给GROUP BY字段加索引ALTERTABLEuserADDINDEXidx_status(status);-- 方案2:改写为子查询(数据量大时)SELECTstatus,cntFROM(SELECTstatus,COUNT(*)AScntFROMuserWHEREid>0GROUPBYstatus)t;

4.5 场景5:大表JOIN效率低

问题SQL:

-- user表1000万行,order表5000万行EXPLAINSELECT*FROMuseruJOIN`order`oONu.id=o.user_idWHEREu.status=1;

优化方案:小表驱动大表

-- 确保驱动表是小结果集EXPLAINSELECT*FROM(SELECT*FROMuserWHEREstatus=1)uJOIN`order`oONu.id=o.user_id;-- 或者使用STRAIGHT_JOIN强制驱动顺序EXPLAINSELECT*FROMuseru STRAIGHT_JOIN`order`oONu.id=o.user_idWHEREu.status=1;

同时确保JOIN字段有索引:

ALTERTABLE`order`ADDINDEXidx_user_id(user_id);

五、慢查询治理体系

5.1 事前:SQL Review

代码审查时,必须检查SQL:

  • 新加的SQL是否走了索引?
  • 是否有全表扫描风险?
  • 是否在大表上做全量操作?
  • 是否用了SELECT *

Review Checklist:

检查项通过标准
是否用到索引EXPLAIN的type至少为range
是否扫描过多数据rows预估 < 10000
是否有filesortExtra不包含Using filesort
是否有temporaryExtra不包含Using temporary
是否SELECT *只查询需要的字段

5.2 事中:实时监控

搭建 Prometheus + Grafana 慢查询监控大盘:

# Prometheus 配置-job_name:'mysql'static_configs:-targets:['localhost:9104']

关键监控指标:

指标名告警阈值含义
mysql_global_status_slow_queries> 10/分钟慢查询数量
mysql_global_status_threads_running> 50正在执行的线程
mysql_global_status_innodb_row_lock_waits> 10/分钟行锁等待次数

5.3 事后:定期分析

每周慢查询TOP10分析报告模板:

1. 查询SQL 2. 执行次数 / 平均执行时间 3. 扫描行数 / 返回行数 4. EXPLAIN分析 5. 优化建议 6. 优化后预计提升

5.4 治理流程

发现慢查询 → EXPLAIN分析 → 定位原因 → 优化SQL/加索引 → 验证效果 → 监控持续观察 ↑ | └──────────────── 定期巡检,形成闭环 ────────────────────────────┘

六、踩坑指南

坑1:EXPLAIN的rows是预估不是实际

我踩过这个坑。开发环境数据量小,EXPLAIN显示rows=100,信心满满地上线。结果生产环境800万数据,扫描了100万行。一定要在生产环境的从库上验证!

坑2:开发环境和生产环境EXPLAIN结果差异大

开发库就几百条数据,优化器可能选择全表扫描。生产库几百万数据,同样的SQL可能需要走索引。务必在数据量相近的环境验证。

坑3:索引加对了但执行计划不走索引

有时候明明有索引,EXPLAIN显示key=NULL。大概率是统计信息过期了,执行:

ANALYZETABLEuser;

更新统计信息后,优化器就会选择正确的索引。

坑4:优化器选择错误

极少数情况下,优化器会选错执行计划。可以用FORCE INDEX强制走索引:

SELECT*FROMuserFORCEINDEX(idx_phone)WHEREphone='13800138000';

但这只是应急手段,不建议常态化使用。


七、问题与解答

Q1:EXPLAIN和EXPLAIN ANALYZE有什么区别?

A:

EXPLAIN只显示执行计划(预估),不真正执行SQL。

EXPLAIN ANALYZE(MySQL 8.0.18+)会真正执行SQL,显示实际执行时间和实际扫描行数。

-- 仅看执行计划EXPLAINSELECT*FROMuserWHEREid=1;-- 真正执行并分析(注意:会实际跑SQL!)EXPLAINANALYZESELECT*FROMuserWHEREid=1;

生产环境慎用EXPLAIN ANALYZE,特别是UPDATE/DELETE!

Q2:为什么加了索引,查询还是很慢?

A:

可能的原因:

  1. 索引没用到:检查EXPLAIN的key列是否为NULL
  2. 回表次数太多:SELECT * 导致大量回表,考虑覆盖索引
  3. 数据量太大:即使走索引,扫描行数还是很多,考虑分表分库
  4. 索引选择性差:比如性别字段(只有男/女),索引效果很差
  5. 服务器负载高:磁盘IO打满,CPU飙高,再好的索引也白搭

Q3:慢查询日志记录了太多数据,怎么过滤?

A:

可以通过配置过滤:

# 只记录超过10秒的慢查询 long_query_time = 10 # 不记录管理语句(如ALTER TABLE) log_slow_admin_statements = 0 # 不记录从库的慢查询 log_slow_slave_statements = 0

或者使用pt-query-digest的过滤参数:

# 只分析查询时间超过5秒的pt-query-digest--filter'$event->{Query_time} > 5'/var/log/mysql/slow.log

八、面试高频考点汇总

考点1:EXPLAIN中type列有哪些值?性能排序是怎样的?

答案:

system > const > eq_ref > ref > range > index > ALL
  • system/const:主键或唯一索引,性能最好
  • eq_ref:JOIN时主键关联
  • ref:普通索引等值查询
  • range:索引范围扫描
  • index:全索引扫描
  • ALL:全表扫描,性能最差

考点2:Extra列中Using filesort和Using temporary代表什么?

答案:

  • Using filesort:MySQL无法利用索引完成排序,需要额外排序操作。通常是因为ORDER BY字段没有索引,或者不符合最左前缀。
  • Using temporary:需要创建临时表来保存中间结果。常见于GROUP BY、DISTINCT、UNION等操作。

两者都是性能警告,需要优化。

考点3:什么是覆盖索引?

答案:

覆盖索引是指查询的所有字段都在索引中,不需要回表查数据。

-- 索引:idx_name_age (name, age)SELECTname,ageFROMuserWHEREname='张三';-- 只需要查索引树就能拿到所有数据,Extra显示Using index

优点:减少回表IO,大幅提升查询性能。

考点4:索引失效的常见场景有哪些?

答案:

  1. 对索引字段做函数操作(YEAR(create_time)
  2. 隐式类型转换(字符串字段传数字)
  3. like前缀模糊('%张三%'
  4. 不符合最左前缀原则
  5. 索引字段参与计算(id + 1 = 100
  6. OR条件中部分字段无索引
  7. 全表扫描比索引更快时(数据量极小)

考点5:如何分析一条慢SQL?

答案:

  1. EXPLAIN分析执行计划,看type、key、rows、Extra
  2. 查看是否走索引,没走索引就分析原因
  3. 查看扫描行数,rows是否过大
  4. 查看Extra,是否有filesort或temporary
  5. 查看慢查询日志,确认执行时间和频率
  6. 对比优化前后,用EXPLAIN验证效果

九、模拟面试官提问和参考答案

场景题1:生产环境CPU飙高,你如何判断是不是慢查询导致的?

参考答案:

  1. 先看监控,确认CPU飙高的时间点
  2. 登录MySQL,执行SHOW PROCESSLIST,看是否有大量 “Sending data”、“Sorting result” 状态的线程
  3. 查看慢查询日志,定位该时间段的慢查询
  4. EXPLAIN分析可疑SQL的执行计划
  5. 如果是慢查询导致,临时杀掉慢查询线程(KILL query_id),然后长期优化SQL或加索引

场景题2:有个分页查询LIMIT 1000000, 10很慢,怎么优化?

参考答案:

  1. 延迟关联:先查id,再JOIN取数据

    SELECT*FROMuseruJOIN(SELECTidFROMuserORDERBYidLIMIT1000000,10)tONu.id=t.id;
  2. 覆盖索引:确保子查询只查索引字段

  3. 业务限制:不允许跳页太深,最多翻到100页

  4. 记录上次位置:用WHERE id > last_id LIMIT 10替代深度分页

场景题3:表有联合索引 (a, b, c),以下SQL能否用到索引?

WHEREa=1ANDb=2ANDc=3;-- 能,全部用到WHEREa=1ANDb=2;-- 能,用到a,bWHEREa=1ANDc=3;-- 能,只用到a(c断了)WHEREb=2ANDc=3;-- 不能,最左前缀断了WHEREa=1ANDb>2ANDc=3;-- 能,用到a,b(b是范围,c用不到)

场景题4:索引加了,但EXPLAIN显示不走索引,可能是什么原因?

参考答案:

  1. 统计信息过期 →ANALYZE TABLE更新
  2. 数据量太小,全表扫描更快
  3. 查询条件用了函数或隐式转换,导致索引失效
  4. 索引选择性太差(如性别字段)
  5. 查询范围太大,回表成本高于全表扫描
  6. 使用了!=<>NOT IN等操作

场景题5:如何设计一套慢查询治理方案?

参考答案:

  1. 事前预防:SQL Review + EXPLAIN检查 + 索引规范
  2. 事中监控:Prometheus+Grafana监控慢查询数量、执行时间
  3. 事后分析:每周慢查询TOP10报告,持续跟踪优化效果
  4. 应急机制:自动告警 + 自动KILL超长查询 + 限流降级
  5. 团队规范:代码提交必须附带EXPLAIN结果 + 索引变更流程

十、互动话题

你在工作中遇到过最离谱的慢查询是什么?排查了多久才找到原因?欢迎在评论区分享你的"翻车"经历,咱们一起复盘!


十一、参考资料

  1. MySQL官方文档 - EXPLAIN输出格式
  2. MySQL官方文档 - 慢查询日志

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询