✅ 精简版
顺序:
- 先拿到慢 SQL:从慢查询日志、数据库监控(Prometheus/Grafana、阿里云 RDS 慢 SQL)找到这条 SQL;
explain看执行计划,重点看 type、key、rows、Extra;- 看是否缺索引、索引失效、回表过多、全表扫描、索引覆盖;
- 再判断是不是锁等待:看
show processlist/performance_schema,有没有长事务、行锁、表锁阻塞; - 然后看服务器资源:CPU、内存、磁盘 IO,是不是数据库资源打满;
- 再看业务层面:是否数据量暴涨、分页深度、join 笛卡尔积,统计信息过时;
- 最后验证优化效果,压测观察。
完整排查步骤
1. 捕获慢 SQL(第一步,找到是哪条 SQL)
- 开启 MySQL慢查询日志 slow_query_log ,设置阈值比如
long_query_time=1s,超过 1 秒自动记录。 - 云数据库(RDS)直接后台看慢 SQL 报表。
- 也可以通过
show processlist实时看当前正在执行的长耗时 SQL。
注意:
show processlist只能看到正在运行的,已经执行完的要看慢日志。
重点看四个指标:执行时间/扫描行数/返回行数/排序、临时表或者锁等待如果扫描了几百万行却返回了十几行,可能是过滤条件没有索引;
如果扫描的数据不多但是扫描时间很长,可能是锁等待。
2. explain 执行计划(最核心,必讲)
执行 explain select xxx,重点看这几个字段:
- type (优先级最高)
system > const > eq_ref > ref > range > index > ALL
ALL:全表扫描,大表绝对红线,必须优化range:范围查询,比如 where id>100,尚可,注意范围不要过大ref:普通索引等值匹配,正常
- key :实际使用的索引,null 代表没有用到索引
- rows:预估扫描行数,数字越大越危险,预估要扫描多少行才找到数据
- Extra(额外信息,坑基本在这里)
Using filesort:文件排序,没用到索引排序,需要额外排序,性能差Using temporary:创建临时表,常见于 group by,消耗大Using index:索引覆盖,最佳!不需要回表Using where:存储引擎返回数据后,MySQL 服务层再过滤
主要看标:
1.全表扫描还是走索引?
2.是否回表过多?(比如,避免select *, 设计覆盖索引)
3.排序和分页?(比如深分页,游标分页)
索引失效常见原因(面试官最爱追问)
- 索引列做函数运算、隐式类型转换(字符串不加引号)
- like
%前缀通配like '%abc' - or 两边有非索引字段
- 联合索引最左匹配原则不满足
- MySQL 优化器判断走索引代价更大,主动放弃索引,走全表扫描(比如筛选后数据占表 30% 以上)
什么是回表?
前提:InnoDB 索引分两种:主键索引(聚簇索引) 、二级索引(普通索引)
二级索引里面存的不是完整行数据,只存【索引列 + 主键】。通过二级索引找到主键之后,再拿主键去主键索引里读取完整行数据,这个二次查找的动作就叫回表。
1. InnoDB 聚簇索引原理
InnoDB 主键索引就是聚簇索引:
- 叶子节点 = 完整一行的所有数据
- 表数据本身就放在主键索引的叶子节点,数据和主键索引绑定在一起
二级索引(普通索引、唯一索引):
- 叶子节点只存:索引字段的值 + 主键 ID ,没有其他业务字段
举个例子
order表:id(主键), orderNo, user_id, amount建立普通索引:idx_orderNo(orderNo)执行 SQL:
sqlselect * from `order` where orderNo = 'ORD123456';执行过程:
- 在二级索引
idx_orderNo找到orderNo='ORD123456',拿到对应的主键id=10001- 拿着 id=10001 去主键索引(聚簇索引)叶子节点,读取整行全部字段 👉 第二步这个动作就是回表
回表本质:两次 B + 树查找,一次二级索引,一次主键索引,会产生额外 IO,影响性能。
2. 什么情况不会回表?------ 索引覆盖(Using index)
查询需要的所有字段,全部都包含在二级索引里面,不需要去主键索引查数据,就不会回表。
把索引改成联合索引:
idx_orderNo(orderNo, user_id, amount)
sqlselect orderNo,user_id,amount from `order` where orderNo='ORD123456';explain Extra:
Using index,直接从二级索引拿到全部需要字段,不需要回表。3. 什么时候回表代价很高?
- 查询出来结果集很大,大量行都要回表,大量随机 IO
- 这也是 MySQL 优化器有时候有索引但是不走索引的原因: 如果筛选出大量数据,回表太多,优化器评估:直接全表顺序扫描比大量随机 IO 回表更快,于是放弃索引。
✅精简版
InnoDB 主键索引是聚簇索引,叶子节点保存完整行数据;
二级索引叶子节点只存索引列 + 主键。
当我们通过二级索引查询,需要的字段不在二级索引里面,就需要拿着主键去主键索引再次查找完整数据,这个过程叫回表。
如果查询的所有字段都在二级索引里,就是索引覆盖,避免回表,性能更好。
高频追问
Q1:主键查询会不会回表?
A:不会。主键直接走聚簇索引,一次 B + 树查找直接拿到完整行,不存在回表。
Q2:联合索引什么时候触发回表?
A:where 条件命中索引,但是
select要查询的字段不在联合索引里面,就需要回表。Q3:回表是随机 IO 还是顺序 IO?
A:随机 IO。二级索引查出来的主键大概率是离散无序的,去主键索引读取数据的时候,磁盘指针到处跳,随机 IO 开销远高于顺序扫描。
Q4:为什么 MyISAM 没有回表概念?
A:MyISAM 是非聚簇索引,主键索引和二级索引结构一样,叶子节点存的都是磁盘文件偏移地址,所有索引查找都是拿偏移量读文件,没有聚簇索引这个概念,所以不存在回表。
3. 排查锁 (锁等待、死锁)& 长事务
SQL 本身逻辑很快,但是被锁住,表现为查询卡住
sql
show engine innodb status;
- 长事务:事务开启很久不提交,占用行锁,阻塞后面的 DML / 查询(RR 隔离级别下 MVCC 读快照,普通 select 不加行锁;update/delete 会被阻塞)
注意:普通快照读不会被行锁阻塞;当前读(select ... for update)会被行锁阻塞。
- 存在表锁、意向锁,大量事务等待锁,出现锁等待、事务回滚。
4. 服务器资源层面
- CPU:MySQL CPU 100%,大概率大量排序、全表扫描、复杂计算
- IO:磁盘 IO 打满,大量随机 IO,回表过多、缺少索引
- 内存:buffer pool 太小,热点数据放不到内存,大量磁盘读取
5. 业务 SQL 写法问题
- 深度分页:
limit 100000,10,需要扫描前面十万行;优化:主键 id 分页where id>xxx limit 10 - join 关联没加索引,产生笛卡尔积,行数爆炸
- group by /order by 字段无索引,触发 Using filesort/temporary
- 一次性查询大量字段,不 select *,减少回表
- 统计信息过时,优化器选错索引(
analyze table更新统计信息)
6. 优化之后验证
优化后再次 explain,观察 type、rows;上线前压测,监控 QPS、RT、慢查询数量变化。
高频追问(背)
Q1:Using filesort 一定很差吗?
不一定。小数据量 filesort 影响很小;大表、大结果集出现 filesort 才是严重问题。本质是排序字段没有索引,MySQL 要在内存 / 磁盘做排序。
Q2:select * 有什么坏处?
- 读取不需要的字段,增加网络传输;
- 无法触发索引覆盖,必须回表,增加 IO;
- 表结构变更,业务容易出问题。
规范:只查业务需要的字段。
Q3:联合索引最左匹配原则是什么?
联合索引
idx_a_b_c(a,b,c),查询条件必须从左开始连续匹配 。 where a=? ✔ where a=? and b=? ✔ where b=? ❌ 跳过左边 a,索引失效 范围查询后面的字段不能走索引 :where a=? and b>10 and c=?,c 无法使用索引。
Q4:什么是索引覆盖?
查询需要的所有列,全部都在索引里面,不需要回主键索引查原始数据,Extra 显示 Using index,性能很高。 比如联合索引包含 select 后面所有字段 + where 条件字段。
Q5:为什么有时候有索引,但是 MySQL 不走索引?
MySQL 优化器会估算成本:如果这条 SQL 查询结果占表数据很大比例(比如 > 20%~30%),走索引需要大量回表随机 IO,优化器判断全表顺序扫描更快,主动放弃索引。
Q6:慢查询日志开启对性能影响大吗?
有一点点开销,线上生产环境一般可以开启,阈值合理设置(1s),只记录慢 SQL,不会大量写入。
Q7:MVCC 和锁的关系?普通 select 会不会被行锁阻塞?
InnoDB RR 隔离级别,普通 select 是快照读,读取历史版本,不加行锁,不会被行锁阻塞; select ... for update /select ... lock in share mode 是当前读,会加锁,会被阻塞。