一条原本几十毫秒就能跑完的SQL,在数据量涨到几百万行之后突然变成两秒多,应用层开始超时报警。遇到这种情况,我的第一反应不是去调代码、加缓存,而是先把这条SQL的执行计划拿过来看。
MySQL执行计划就是优化器在真正执行SQL之前做的一份“操作方案”:它打算先访问哪张表、用哪个索引、大概扫描多少行、以什么顺序把多张表的数据拼接起来。方案选对了,一条SQL可以快上几个数量级;方案选歪了,再好的硬件也扛不住。很多同学在慢SQL优化上卡壳,不是因为不会写SQL,而是因为看不懂执行计划里那些字段——type、rows、Extra明明都认识,但拼在一起并不知道在说什么。
这篇文章我把执行计划从产生原理到实战排查串起来讲一遍,包括优化器内部是怎么算账的、EXPLAIN输出的每一列该怎么读、type访问路径背后对应多少真实工作量,以及几个从线上慢SQL一路改到执行计划变好的完整案例。不管是刚接触MySQL的新手,还是已经在做SQL优化的开发者,都可以照着这套思路去定位问题。
1. 优化器是怎么算出一份执行计划的
1.1 MySQL的优化器拿到一条SQL后,真正做了什么
先想一个问题:SQL语句本身描述的是“我想要什么结果”,而不是“你应该怎么取数据”。
比如 SELECT * FROM t_order WHERE user_id = 100 AND amount > 100,这句话只表达了三个约束条件:查订单表、用户ID是100、金额大于100。但至于先按user_id过滤还是先按amount过滤、用哪个索引、是走二级索引回表还是直接扫主键,SQL语句里一个字都没提。这些决定全部由MySQL的优化器来做。
整个流程大概是这样的:SQL进来后,第一步是词法分析和语法解析,把字符串变成一棵语法树,这个过程会检查SQL写得对不对;第二步是预处理,校验表名、字段名是否存在,做权限检查之类的语义校验;第三步才进入真正的优化阶段,生成多个候选执行计划,估算每个计划的成本,选一个成本最低的;最后执行器按照选定的计划去存储引擎里取数据。
这里要建立一个基本认知:执行计划不是SQL执行完之后才产生的,而是执行之前优化器给出的预案。就像你开车去一个陌生地点,导航软件在出发前会计算几条路线,预估每条路的耗时,然后推荐一条。MySQL的优化器干的就是导航的工作,但它计算的不是红绿灯和堵车时间,而是扫描多少行数据、读多少个数据页。
1.2 成本模型:优化器这笔账是怎么“算”出来的
MySQL优化器的核心决策依据是一个成本模型,它给数据库里的各种操作都定义了代价。简化来看,成本主要由两部分构成:一个是IO成本,读一个数据页算作一次IO成本;一个是CPU成本,每比较、每计算一行记录都算作一次CPU成本。
例如一条查询如果用主键定位一行数据,成本就非常低,因为InnoDB的聚簇索引叶子节点就是完整的数据行,通过主键可以直接定位到那一页;但如果做一个全表扫描,优化器会估算表里一共有多少行、这些数据分布在多少个数据页上,然后把这些页的IO成本全部累加。
为了做这个估算,优化器需要依赖一些基础统计信息,比如表的行数、索引的基数(cardinality)、索引的选择性等。这些信息来源于MySQL维护的统计字典,有些来自 SHOW TABLE STATUS,有些来自 information_schema.STATISTICS 里的 CARDINALITY。
问题来了:统计信息终究是“抽样估算”,不是精确值。如果表数据频繁增删改,而统计信息没有及时更新,优化器拿到的就可能是一个过期的行数,算出来的成本自然就不准。这就是为什么后面我会专门讲统计信息失真对执行计划的影响——很多执行计划看起来“不聪明”,问题恰恰出在它拿到的基础数据就是错的。
1.3 为什么执行计划也有可能与实际不符
优化器在计算成本的时候,面对的数据不是真实数据,而是统计信息。统计信息不够准确时,优化器就会“猜错”。举几个很典型的例子:
- 表实际有500万行,但优化器统计信息还停留在上一次ANALYZE时的80万行;
- 某列的索引基数明明很高,但统计信息里显示的数据分布已经过时;
- 查询条件里写了函数或者发生了隐式类型转换,优化器无法准确判断条件能过滤掉多少行,只好给一个比较悲观的估算。
执行计划只是基于估算的最优解,不是实测的最优解。这也是为什么业界一直强调“慢SQL优化要以真实执行计划而非直觉为准”,但同时也别忘了,估算型执行计划有天然误差。后面我会单独讲如何用 EXPLAIN ANALYZE 拿到真实执行数据,把误差暴露出来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. EXPLAIN每一列都要看得懂:从select_type到Extra怎么读
2.1 EXPLAIN的三种输出格式与基本用法
MySQL查看执行计划的入口是EXPLAIN语句,基本用法就是在你的SELECT前加上EXPLAIN关键字。比如:
sql复制EXPLAIN SELECT * FROM t_order WHERE user_id = 100;
EXPLAIN不会真正执行这条查询,它只是在优化器完成决策后,把执行计划以表格形式输出给你。线上排查的时候,这条语句几乎不会对数据库产生额外压力,可以放心用。
MySQL支持三种输出格式,适用场景不太一样:
- 传统表格格式:一行对应一个表访问步骤,最直观,日常排查主力;
FORMAT=JSON:输出结构化的JSON,里面带cost成本信息,适合需要看优化器决策细节的场景;FORMAT=TREE:输出树状格式,层级关系比表格更清楚,适合理解多表join的执行顺序。
我实际最常用的还是传统格式,因为一眼扫过去就能定位type和k
