大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
有个经典的"凌晨惊魂"场景:某条核心SQL跑了半年都没问题,每天几十万次执行,响应时间稳定在5毫秒以内。某天凌晨三点,监控告警疯狂弹窗——这条SQL突然飙到5秒,CPU打满,整个系统雪崩。
DBA赶到现场,第一反应是看代码有没有人改。没有。看索引有没有人删。没有。看数据量有没有暴涨。也没有。
最后查下来,原因让人无语:统计信息过期了。数据库优化器拿着一份"过时的情报",给这条SQL选了一条错误的执行计划。
今天把执行计划突变的底层原理、排查手段和预防机制,一次讲清楚。
一、先搞懂几个概念
执行计划(Execution Plan):数据库执行一条SQL的具体步骤。先走哪个索引、先JOIN哪张表、用什么JOIN方式(Nested Loop、Hash Join、Merge Join),这些决策组合起来就是执行计划。
优化器(Optimizer):数据库里的"决策引擎",负责为每条SQL选择最优的执行计划。它不跑SQL,只"猜"哪种执行方式最快。
统计信息(Statistics):优化器做决策的依据。包括表的总行数、每列的数据分布(最大值、最小值、NULL占比、直方图)、索引的选择性(不同值的数量)等。本质上就是优化器眼中的"数据库快照"。
基数估计(Cardinality Estimation):优化器预估每一步会返回多少行数据。预估准了,执行计划就优;预估偏了,就可能选错索引、选错JOIN顺序。
CBO(Cost-Based Optimizer):基于成本的优化器。优化器根据统计信息计算每种执行计划的"成本"(CPU消耗、IO次数、内存占用),选成本最低的那个。
理解了这些概念,就能回答一个核心问题:为什么执行计划会突然变?
二、执行计划为什么会"背叛"你
统计信息过期:优化器拿到的是"过期情报"
这是最常见的原因。统计信息不是实时更新的,大多数数据库是定期收集或手动触发。
假设你有一张订单表,平时100万行,统计信息也是按这个量级收集的。某天大促,数据量涨到500万,但统计信息还没更新。优化器依然认为表里只有100万行——于是选择了全表扫描(因为它觉得100万行全表扫比走索引快)。实际上500万行全表扫描直接卡死。
统计信息 ≠ 实时数据,它是一份"延迟的快照"。
数据倾斜:平均值骗了优化器
即使统计信息是新的,也可能因为数据分布不均匀而误导优化器。
比如一个订单表的status列,99%的数据是COMPLETED,1%是PENDING。如果统计信息只记录了平均分布(没有收集直方图),优化器会认为每个状态的占比差不多。当你查询status = 'COMPLETED'时,优化器预估返回1000行(总行数10万的1/100),实际返回99000行——走索引反而比全表扫慢几十倍。
索引变化:新增索引不一定是好事
开发同学看到慢SQL,第一反应是加索引。加完索引后统计信息更新,优化器重新评估所有可用索引,可能选出一个更差的执行计划。
加索引 ≠ SQL变快,它只是给优化器多了一个选择,而这个选择可能是错的。
参数变更:看似无关的配置调整
optimizer_mode从ALL_ROWS改为FIRST_ROWSoptimizer_features_enable版本升级后行为变化statistics_level从TYPICAL改为BASIC(停止收集部分统计信息)
这些参数调整不会立刻生效,但下一次硬解析时,优化器的决策逻辑可能完全改变。
三、执行计划突变的排查步骤
第一步:确认是不是执行计划变了
-- Oracle
SELECT * FROM v$sql_plan WHERE sql_id = 'your_sql_id';
-- MySQL (8.0+)
EXPLAIN FORMAT=TREE SELECT ...;
-- PostgreSQL
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
-- 对比历史执行计划
-- Oracle: DBMS_XPLAN.DISPLAY_AWR('your_sql_id')
重点对比:访问路径(全表扫 vs 索引扫描)、JOIN顺序、JOIN方式、预估行数 vs 实际行数。
第二步:检查统计信息是否过期
-- Oracle:查看表的统计信息收集时间
SELECT table_name, last_analyzed, num_rows, blocks
FROM user_tables
WHERE table_name = 'YOUR_TABLE';
-- MySQL:查看InnoDB表统计信息
SHOW TABLE STATUS LIKE 'your_table';
-- PostgreSQL:查看统计信息
SELECT last_analyze, last_autoanalyze, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'your_table';
如果last_analyzed是几天甚至几周前,而这段时间数据变化超过10%,基本可以判定统计信息过期。
第三步:对比预估行数和实际行数
这是判断优化器是否"误判"的关键指标。
-- 执行SQL时开启实际执行统计
-- Oracle: EXPLAIN PLAN FOR ... 然后 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- MySQL: EXPLAIN ANALYZE SELECT ...;
-- PostgreSQL: EXPLAIN ANALYZE SELECT ...;
如果某一步的预估行数(estimated rows)和实际行数(actual rows)相差10倍以上,说明基数估计严重失真,执行计划很可能选错了。
第四步:检查是否有绑定变量窥探问题
绑定变量第一次执行时,优化器会"窥探"变量值来生成执行计划。后续执行直接复用这个计划,即使变量值的数据分布差异很大。
比如第一次传的是status = 'PENDING'(只有100行),优化器选了索引扫描。后面传的是status = 'COMPLETED'(99000行),还是走索引——但全表扫描反而更快。
四、预防执行计划突变的4种手段
手段一:合理设置统计信息收集策略
不要完全依赖自动收集,根据业务特点定制:
| 策略 | 适用场景 | 收集频率 |
|---|---|---|
| 自动收集 + 默认阈值 | 数据变化平稳的普通表 | 系统自动触发 |
| 手动定时收集 | 数据批量导入/删除的表 | 每天凌晨或批量操作后 |
| 锁定统计信息 | 历史归档表(数据不变化) | 收集一次后锁定 |
| 收集直方图 | 数据分布严重倾斜的列 | 按需收集 |
-- Oracle: 手动收集统计信息(含直方图)
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA', 'TABLE_NAME',
method_opt => 'FOR COLUMNS SIZE AUTO skewed_column');
-- MySQL: 手动分析表
ANALYZE TABLE your_table;
-- PostgreSQL: 手动分析
ANALYZE your_table;
手段二:使用执行计划基线(Plan Baseline)
Oracle提供了SQL Plan Management(SPM),可以把"好的执行计划"锁定下来,即使统计信息变化也不让优化器切换到更差的计划。
-- Oracle: 创建执行计划基线
DECLARE
l_plans_loaded PLS_INTEGER;
BEGIN
l_plans_loaded := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
sql_id => 'your_sql_id');
END;
金仓数据库也提供了类似的执行计划管理能力。通过DBMS_SPM兼容包,可以将经过验证的优秀执行计划固定下来,避免因统计信息变化导致的性能波动。同时支持执行计划演化(evolve),在确认新计划更优后才自动切换。
手段三:SQL Profile / Outline
SQL Profile是优化器的"纠正器"。当发现某条SQL的执行计划不理想时,可以创建一个SQL Profile,告诉优化器"这条SQL按这个方式执行"。
-- Oracle: 使用SQL TUNING ADVISOR
DECLARE
l_tuning_task VARCHAR2(30);
BEGIN
l_tuning_task := DBMS_SQLTUNE.CREATE_TUNING_TASK(sql_id => 'your_sql_id');
DBMS_SQLTUNE.EXECUTE_TUNING_TASK(l_tuning_task);
DBMS_SQLTUNE.ACCEPT_SQL_PROFILE(task_name => l_tuning_task);
END;
手段四:监控统计信息变化
建立监控机制,在统计信息过期前主动预警:
-- 找出统计信息超过7天未更新的表
SELECT table_name, last_analyzed,
ROUND((SYSDATE - last_analyzed), 1) as days_since_analyze
FROM user_tables
WHERE last_analyzed < SYSDATE - 7
ORDER BY last_analyzed;
建议在监控系统中加入以下告警项:
- 核心表统计信息超过X天未更新
- 单表数据变化量超过上次统计的10%
- 执行计划发生变更(对比AWR报告)
五、总结
执行计划突变的本质,是优化器拿着一份过时的地图,给你指了一条错误的路。
代码没改、索引没动,SQL突然变慢——不要急着翻代码,先查统计信息。
排查执行计划问题,按这个顺序来:
- 对比执行计划:确认是不是执行计划变了,不是SQL本身的问题
- 检查统计信息:last_analyzed多久了?数据变化量超过10%了吗?
- 预估 vs 实际:基数估计偏差超过10倍,优化器就"失明"了
- 绑定变量:第一次执行的变量值,可能不适合后续的变量值
预防胜于治疗:统计信息收集策略 + 执行计划基线 + 变化监控,三管齐下,让慢SQL扼杀在摇篮里。
小耶在手,SQL不愁。
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~