SQL执行计划的“成本模型”:读懂cost,理解优化器为什么选这个计划

举报
这个DBA有点耶 发表于 2026/08/03 15:39:47 2026/08/03
【摘要】 EXPLAIN能告诉你优化器选了哪个执行计划,但说不出它为什么这么选——明明有索引它却走全表扫描,明明A计划更快它却选了B计划。优化器不靠猜,它靠一套成本模型(Cost Model)做决策。本文从优化器的成本模型出发,拆解cost的构成(IO_cost、CPU_cost、memory_cost),讲解如何通过EXPLAIN FORMAT=JSON和OPTIMIZER_TRACE看到优化器的“思考过

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

做了这么多年SQL优化,你有没有发现一个奇怪的现象:同一套业务数据,同一个版本的数据库,只是在不同的环境里跑,执行计划可能完全不一样——有时候走索引,有时候全表扫描。开发环境跑得好好的SQL,一上生产就慢了。

排除了数据量差异、硬件差异之后,真正的原因指向了一个地方:优化器在不同的环境里“算”出来的成本不一样。

优化器不是凭感觉选执行计划的,它有一套完整的成本模型(Cost Model) 。它会把每种可能的执行方式换算成一个数字——cost,然后选cost最小的那个。问题在于,这个cost是算出来的,不是测出来的。而算的依据,是统计信息

统计信息可以理解成优化器手里的“参考数据”——表有多少行、每列有多少个不同值、数据分布如何。成本模型则是优化器用来算账的“计算器”——读一次磁盘算多少分、处理一行数据算多少分。两者配合,优化器才能算出每个执行计划的cost。

如果统计信息不准,或者成本模型的计算逻辑跟你预想的不一样,优化器的判断就会“跑偏”。

成本模型是怎么“算账”的?

优化器的决策逻辑是:对每个可能的执行计划(走哪个索引、用什么JOIN顺序),用成本模型估算出一个cost,然后对比所有候选计划的cost,选择最小的那个。

这个“代价”主要由三部分构成:

成本类型 含义 说明
IO_cost 读写数据页的成本 从磁盘读取数据页到内存的代价,通常是成本的大头
CPU_cost 处理行数的成本 在内存中处理行数据的代价,比如比较、聚合、排序
memory_cost 临时内存使用成本 使用临时表或排序缓冲区的代价

简单说,优化器会把每个可能的执行计划换算成一个数字——cost,然后选最小的那个。

举个例子:WHERE user_id = 12345 AND order_date > '2026-01-01'。优化器会估算两种方案的成本——走(user_id, order_date)复合索引,或者全表扫描。如果走索引的cost比全表扫描小,优化器就选索引。如果统计信息过旧,走索引的成本估算可能严重偏高,优化器就会“算错账”。

关键认知:优化器不是“跑一遍测试”来比较哪个计划快,而是“算一遍账”来估算哪个计划成本低。这个“账”算得准不准,完全取决于统计信息的准确度。如果统计信息过旧,优化器可能基于错误的数据做出“看起来最优”的决策——实际上它是“看错了”,不是“算错了”。

那怎么看到优化器算的这笔账?

传统的EXPLAIN只告诉你结果,不告诉你“账是怎么算的”。但MySQL 8.0提供了EXPLAIN FORMAT=JSON,能把优化器的成本估算完整地展示出来。

EXPLAIN FORMAT=JSON 
SELECT * FROM orders WHERE user_id = 12345 AND order_date > '2026-01-01';

输出的JSON中,最核心的部分是cost_info

"cost_info": {
  "read_cost": "1500.25",
  "eval_cost": "500.00",
  "prefix_cost": "2000.25",
  "data_read_per_join": "10M"
}
字段 含义
read_cost 读取数据的IO成本
eval_cost 评估和处理行的CPU成本
prefix_cost 当前表在JOIN顺序中的累计成本
data_read_per_join 预估读取的数据量大小

注意cost是优化器基于统计信息估算的相对代价,单位是“等价随机I/O次数”,不是真实执行耗时。但它的相对大小告诉我们优化器为什么选了A没选B。

更进一步:OPTIMIZER_TRACE——看优化器的完整思考过程

EXPLAIN FORMAT=JSON只展示了最终选中的计划的成本,但看不到优化器放弃了哪些计划、为什么放弃

这就是OPTIMIZER_TRACE的价值——它能完整记录优化器从接收SQL到选定执行计划的每一步决策:候选计划评估、规则应用、索引选择、成本对比,全部以JSON格式呈现。

SET SESSION optimizer_trace = "enabled=on";
SELECT * FROM orders WHERE user_id = 12345 AND order_date > '2026-01-01';
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G
SET SESSION optimizer_trace = "enabled=off";

OPTIMIZER_TRACE的输出中,重点关注rows_estimation部分——它会列出优化器为每个候选索引计算的costrows,以及最终为什么选了某个计划。

一个真实案例

某电商系统,订单表1000万行,(user_id, order_date)上有复合索引。某天查询突然变慢,执行计划显示全表扫描。

OPTIMIZER_TRACE查看后发现,优化器估算user_id=12345会返回50万行(实际只有200行)。因为统计信息过旧,优化器认为“走索引回表50万次的IO成本,大于全表扫描1000万行的顺序读成本”,所以选了全表扫描。

执行ANALYZE TABLE orders更新统计信息后,优化器重新估算只返回200行,走复合索引的cost远低于全表扫描,查询从5秒降到了0.05秒。

关键认知:优化器不是“笨”,是“看错了”——它基于错误的统计信息做出了在当时看起来最优的决策。

总结

工具 能看到什么
EXPLAIN 知道“选了谁”
EXPLAIN FORMAT=JSON 知道“选了谁、花了多少钱”
OPTIMIZER_TRACE 知道“为什么选它、为什么没选另一个”

执行计划是优化器的“决策结果”,统计信息是优化器的“决策依据”,成本模型是优化器的“决策算法”。学会用EXPLAIN FORMAT=JSONOPTIMIZER_TRACE看到优化器的“账本”和“思考过程”,你就能从“看懂EXPLAIN”升级到“理解优化器为什么这么选”。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。