大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
上周我们讲了怎么看执行计划——type、key_len、rows、filtered、Extra,每个字段都拆开讲了。但有一个问题一直没回答:优化器是根据什么来决定走哪个执行计划的?
你肯定遇到过这种情况:明明有合适的索引,优化器却选了全表扫描;明明A计划更快,优化器却选了B计划。这时候你可能会骂优化器“笨”,但其实它只是做了一个“基于现有信息的最优判断”——而它依赖的“现有信息”,就是统计信息。
如果统计信息不准,优化器的判断就会出错,最终执行计划就会跑偏。
今天我们把执行计划再往深挖一层,讲清楚统计信息和基数估算到底是怎么回事,以及当执行计划“跑偏”时,怎么从统计信息入手把它拉回来。
一、优化器的决策逻辑:它不是“猜”,而是“算”
数据库优化器是基于代价(Cost-Based) 的。它不会凭感觉选执行计划,而是会计算每种可能的执行方式的“代价”,然后选代价最小的那个。
这个“代价”是怎么算出来的?核心公式可以简化为:
代价 ≈ 扫描行数 × 单行处理成本 + 其他开销
其中“扫描行数”就是基数估算(Cardinality Estimation) ——优化器估算的、某个操作会返回的行数。
而基数估算依赖的数据,就是统计信息——表有多少行、每列有多少个不同值、数据分布如何。
二、统计信息:优化器的“眼睛”
统计信息是优化器做出决策的依据。如果统计信息不准,优化器的判断就会跟着错。
统计信息主要包括以下几类:
| 统计信息类型 | 作用 | 影响 |
|---|---|---|
| 表级行数 | 估算全表扫描的基数 | 决定是否走全表扫描 |
| 列级不同值数量(NDV) | 估算过滤条件的选择率 | 决定索引是否被选中 |
| 列级NULL值比例 | 影响条件判断的准确性 | 影响WHERE条件的估算 |
| 数据分布(直方图) | 了解列值的分布情况 | 影响范围查询的估算 |
举个例子:一张订单表有1000万行,status列有3个不同值(PAID、UNPAID、REFUND)。如果统计信息准确,优化器知道status='PAID'大约占30%,也就是300万行。如果统计信息过旧(比如只统计到表有100万行),优化器会估算status='PAID'只有30万行,从而可能错误地选择索引扫描而非全表扫描——或者反过来。
三、基数估算:优化器的“算力”
有了统计信息,优化器就开始做基数估算——预测每个操作会返回多少行。
基数估算的核心是选择率(Selectivity) 计算。选择率表示某个过滤条件能筛选掉多少数据。
选择率越高,过滤掉的数据越多,返回的行数越少;选择率越低,返回的行数越多。
对于等值条件(WHERE status = 'PAID'),选择率 ≈ 1 / 该列的不同值数量。如果status有3个不同值,选择率约33%。
对于范围条件(WHERE create_time > '2026-01-01'),选择率依赖直方图统计信息来估算数据分布。
如果统计信息不准确,选择率就会算错,基数估算就会偏差,最终执行计划就可能选错。
四、真实案例:统计信息过旧导致的执行计划跑偏
我遇到过这样一个案例:一张日志表有500万行,user_id列上有索引。业务方反馈一条查询突然变慢:
SELECT * FROM user_logs WHERE user_id = 12345 AND create_time > '2026-01-01';
正常情况下,这个查询应该走(user_id, create_time)复合索引,扫描几十行就返回。但实际执行计划显示type=ALL,全表扫描。
查看统计信息后发现,这张表的统计信息还是三个月前收集的——当时表只有50万行。优化器根据旧统计信息估算:user_id = 12345可能返回很多行(因为旧数据中该用户有大量记录),全表扫描可能更“划算”。
执行ANALYZE TABLE user_logs更新统计信息后,优化器重新估算,发现user_id = 12345在500万行中只有几十条记录,复合索引被正确选中,查询从5秒降到了0.05秒。
五、如何检查统计信息是否准确
方法一:对比EXPLAIN的rows与实际行数
执行EXPLAIN后,看rows列的估算值。然后实际执行查询(或使用EXPLAIN ANALYZE),对比实际扫描行数。如果估算值和实际值差了一个数量级以上,统计信息很可能过旧了。
方法二:查看统计信息的最后更新时间
-- MySQL
SELECT table_name, update_time FROM information_schema.tables WHERE table_name = 'your_table';
如果统计信息已经几周甚至几个月没有更新,而表的数据变化很大,就需要执行ANALYZE TABLE。
方法三:查看索引的Cardinality
SHOW INDEX FROM your_table;
Cardinality列显示索引列的不同值数量估算。如果Cardinality与实际明显不符,说明统计信息需要更新。
六、如何维护统计信息
1. 定期执行ANALYZE TABLE
对于数据变化频繁的表,建议设置定时任务每天或每周执行ANALYZE TABLE。MySQL 8.0的统计信息虽然是持久化的,但自动更新阈值默认是表数据变化10%,对于核心业务表,这个阈值可能偏大。
2. MySQL 8.0的直方图支持
MySQL 8.0支持为列创建直方图统计信息,对于数据分布不均匀的列特别有用:
ANALYZE TABLE your_table UPDATE HISTOGRAM ON column_name WITH 100 BUCKETS;
3. 关注统计信息的更新时机
建议在以下操作后执行ANALYZE TABLE:
- 批量数据导入后
- 大量数据删除后
- 表结构变更后(如添加索引)
- 定期维护窗口(如每周日凌晨)
七、进阶:当统计信息准确但执行计划仍然跑偏时
有时候统计信息是准确的,但优化器仍然选择了次优计划。这种情况可能的原因包括:
- 参数设置问题:某些数据库参数(如
eq_range_index_dive_limit)影响优化器的行为 - 优化器提示(Hint) :在极端情况下,可以使用
USE INDEX或FORCE INDEX引导优化器
但在使用Hint之前,建议先用EXPLAIN ANALYZE确认真实执行情况,再判断是否需要强制干预。
执行计划是优化器的“决策结果”,统计信息是优化器的“决策依据”,基数估算是优化器的“决策算法”。这三者形成一个完整的决策链条:统计信息 → 基数估算 → 执行计划。
如果执行计划看起来不合理,不要急着骂优化器“笨”。先检查统计信息是否准确——大概率是优化器“看错了”,而不是“算错了”。学会从这个角度理解执行计划,你就能从“看懂EXPLAIN”升级到“理解优化器为什么这么选”,在SQL优化上真正做到“知其然也知其所以然”。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~