大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
做了这么多年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部分——它会列出优化器为每个候选索引计算的cost和rows,以及最终为什么选了某个计划。
一个真实案例
某电商系统,订单表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=JSON和OPTIMIZER_TRACE看到优化器的“账本”和“思考过程”,你就能从“看懂EXPLAIN”升级到“理解优化器为什么这么选”。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~