SQL执行计划怎么看?DBA手把手教你从头读到尾,3个真实案例实战
凌晨三点,手机响了。线上告警,一条核心业务查询从200毫秒飙到了12秒。爬起来一看,索引明明在,字段也没错。当时我心里就一个念头——得看执行计划。
果不其然,优化器选了个废柴索引,全表扫描87万行。我加了行Hint指定索引,200毫秒回来了。从告警到恢复,前后8分钟。
干了15年数据库,这种事经历太多了。80%的SQL性能问题,看一眼执行计划就能定位。 可偏偏很多开发兄弟不看,上来就加索引、改SQL,运气好碰对了,运气差越改越慢。
今天我把看执行计划的方法掰开了讲,从Oracle到MySQL,结合3个真实踩坑案例。各位看完,至少能做到:SQL慢了,知道往哪看。
执行计划是啥?简单说就是SQL的"施工图纸"
你写了一条SQL,数据库不会直接跑。它要先"规划"一下:先查哪张表、怎么关联、用哪个索引、数据怎么传。这个规划出来的方案,就是执行计划。
Oracle用 DBMS_XPLAN.DISPLAY 看,输出长这样:
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost | Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 45 | 00:00:01 |
|* 1 | TABLE ACCESS FULL | ORDERS| 10 | 45 | 00:00:01 |
---------------------------------------------------------------------------
主要看三个东西:Operation(做了什么操作)、Rows(估算返回多少行)、Cost(代价多大)。
MySQL用 EXPLAIN 看,格式不一样,但思路相通。MySQL重点关注:
- type列:访问方式。从好到差依次是
system > const > eq_ref > ref > range > index > ALL。看到ALL基本就是全表扫描,得警惕。 - key列:实际用了哪个索引。NULL表示没用索引。
- rows列:估算扫描行数。这个数越大,SQL通常越慢。
- Extra列:额外信息。出现
Using filesort或Using temporary要注意,说明有额外排序或临时表开销。
Oracle的Rows对应MySQL的rows列,Cost没有直接对应但思路一样。两个平台的"语言"不同,但"语法"是一回事。
3个真实案例,手把手带你读
案例一:索引选错了,百万级表查询12秒
一条订单查询SQL,按理说走了索引应该很快,但跑了12秒。
EXPLAIN SELECT * FROM orders
WHERE user_id = 10086 AND status = 'paid'
ORDER BY created_at DESC LIMIT 20;
看执行计划:type是 range,但key用的是 idx_created_at,扫描了87万行。
问题很清楚——优化器选错了索引。应该走 (user_id, status) 的联合索引,结果走了 created_at 的单列索引。ORDER BY created_at DESC 可能干扰了优化器的判断。
强制指定索引后:rows 从87万降到23,耗时从12秒降到50毫秒。
后来排查发现,这张表的统计信息已经两个月没更新了。优化器基于过时的数据分布做判断,选错索引太正常了。
教训: 统计信息过期是索引选错的头号原因。大表要定期更新统计信息。
案例二:两表关联,走错了连接方式
两张表JOIN,用户表50万行,订单表2000万行。SQL跑了将近3分钟。
看执行计划:驱动表是订单表(大表),对每条记录去用户表做主键查找。这是典型的Nested Loop,而且驱动表选反了。
优化思路:让小表(用户表)做驱动,大表走索引。或者直接用Hash Join,两张大表之间Hash Join往往比Nested Loop快得多。
调整后走Hash Join,耗时从3分钟降到200毫秒。
教训: 大表关联时,先看驱动表选对没有。小结果集驱动大结果集,这是基本原则。
案例三:子查询拖慢整条SQL
一个看似简单的查询,“查下过单的用户信息”:
SELECT * FROM users
WHERE user_id IN (
SELECT user_id FROM orders WHERE amount > 1000
);
执行计划里出现了一个刺眼的词:DEPENDENT SUBQUERY。这意味着外层每查一行users,内层都要重新执行一次orders的子查询。users表有10万行,子查询就跑了10万次。
改成JOIN写法:
SELECT DISTINCT u.* FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.amount > 1000;
执行计划变成两个表的Hash Join,从30秒降到0.8秒。
教训: 看到 DEPENDENT SUBQUERY 要立刻警觉。90%的情况改成JOIN都能解决。
快速看懂执行计划的三步法
15年看下来,我总结了一套三步法,Oracle和MySQL通用。
第一步:找驱动表
执行计划从最内层开始执行。从ID最小的操作开始,顺着Operation往"右下方"找,找到最底层的那个扫描操作——那就是驱动表。
第二步:看访问方式
const/eq_ref:主键或唯一索引等值查找,通常很快ref:非唯一索引等值,还行range:索引范围扫描,关注范围大小index:全索引扫描,比全表扫描好一点但有限ALL:全表扫描,大表上出现基本要优化
同时看Extra:Using filesort(额外排序)和 Using temporary(临时表)出现就要警惕。
第三步:看连接方式和顺序
- Nested Loop:适合小表驱动大表,被驱动表要有索引
- Hash Join:适合大表之间关联,不需要索引
- Sort Merge Join:已排序数据之间的关联,场景较少
连接顺序很重要。Oracle里从内到外读,MySQL里从上到下读。小结果集要放在驱动位置。
执行计划也有"坑"
执行计划不是万能的,有几个常见的误导场景。
统计信息过时,优化器估算的行数和实际行数差几个数量级。比如Oracle里某个字段NDV(不同值数量)没更新,估算100行,实际50万行。
隐式类型转换让索引失效。WHERE varchar_col = 12345,MySQL会隐式加 CAST,索引直接废了。Oracle也有类似问题。字段是什么类型,条件就传什么类型。
参数嗅探(Parameter Sniffing)问题。存储过程第一次编译时,恰好用了个不具代表性的参数值,后面每次执行都沿用第一次的计划。Oracle和SQL Server都有这个问题。
实战建议
这些年我调SQL,形成了一套固定流程:
- 先看执行计划,别上来就加索引
- 重点关注三个维度:访问方式(ALL?)、连接方式(选对没?)、行数估算(准不准?)
- 统计信息是根本。很多看似复杂的性能问题,更新统计信息就解决了
- 隐式转换是隐形杀手。字段类型不匹配,再好的索引也白搭
- 学会用Hint验证思路。不是让你线上用Hint,而是用它来验证你的优化方向对不对
上个月有个国产数据库迁移项目,同样的SQL在Oracle上跑得好好的,迁过去性能断崖式下跌。看了两边的执行计划,发现是统计信息采集策略不同,优化器选了完全不一样的路径。手动调整统计信息后解决了。
不管用什么数据库,统计信息管理永远是性能调优的第一步。执行计划是你的"诊断报告",学会读它,SQL调优就入门了。
各位平时看执行计划遇到过什么疑难杂症?评论区聊聊。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)