执行计划突变是优化器基于统计信息等重新评估的结果,需用dba_hist_sql_plan定位变更时间点,再通过spm锁定稳定计划;不可依赖v$sql_plan或dba_hist_sqlstat单独判断。
直接结论:执行计划突变不是“坏了”,而是优化器基于当前统计信息、绑定变量值、对象结构等重新评估后的结果;靠猜没用,得用 dba_hist_sql_plan 和 dba_hist_sqlstat 定位变更时间点,再用 spm 锁定稳定计划。
查执行计划历史变更:用 DBA_HIST_SQL_PLAN 看哪天换的算子
执行计划突变的第一手证据在 AWR 快照里,不是看当前 V$SQL_PLAN —— 那只反映“现在正在用哪个”,不记录“以前用过哪些”。
-
DBA_HIST_SQL_PLAN存的是每个快照周期内 SQL 的实际执行计划树(含OPERATION、OPTIONS、OBJECT_NAME) - 关键字段是
PLAN_HASH_VALUE和TIMESTAMP,同一SQL_ID出现多个不同PLAN_HASH_VALUE就说明计划变了 - 执行下面语句能快速定位变更时间点:
SELECT DISTINCT SQL_ID, PLAN_HASH_VALUE,
TO_CHAR(TIMESTAMP, 'yyyymmdd hh24:mi:ss') AS TIMESTAMP
FROM DBA_HIST_SQL_PLAN
WHERE SQL_ID = 'your_sql_id'
ORDER BY TIMESTAMP;
注意:TIMESTAMP 是快照生成时间,不是 SQL 执行时间;如果返回多行且 PLAN_HASH_VALUE 不同,就说明计划在对应时间点发生了切换。
对比两个计划差异:重点看 OPERATION、OBJECT_NAME、COST
拿到两个不同 PLAN_HASH_VALUE 后,不能只看 cost 数值大小,要逐行比对物理算子和访问路径是否一致。
- 执行如下语句拉出两个计划的完整树结构:
SELECT PLAN_HASH_VALUE, ID, OPERATION, OPTIONS, OBJECT_NAME, COST,
TO_CHAR(TIMESTAMP, 'yyyymmdd hh24:mi:ss') AS TIMESTAMP
FROM DBA_HIST_SQL_PLAN
WHERE SQL_ID = 'your_sql_id'
AND PLAN_HASH_VALUE IN (123456789, 987654321)
ORDER BY PLAN_HASH_VALUE, ID;
- 重点关注这几处是否变化:
- 连接方式从
HASH JOIN变成NESTED LOOPS - 索引访问从
INDEX RANGE SCAN变成FULL TABLE SCAN -
OBJECT_NAME指向了临时表、物化视图或分区子对象(说明统计信息可能只更新了部分分区) -
COST值突然跳变但没对应物理操作变化(可能是统计信息采样率异常)
- 连接方式从
不要依赖 DBMS_XPLAN.DISPLAY_AWR 的默认输出——它默认只显示最新一个快照的计划,必须显式指定 plan_hash_value 参数才能对比历史版本。
锁定计划防再次突变:用 DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE 导入 + ENABLED = YES + FIXED = YES
找到性能好的那个 PLAN_HASH_VALUE 后,不能只靠 hint 或 outline,要用 SQL Plan Management(SPM)做持久化锁定。
- 先确认该计划还在共享池中(即当前
V$SQL里还能查到):
SELECT SQL_ID, PLAN_HASH_VALUE, EXECUTIONS, ELAPSED_TIME/1000/EXECUTIONS AS AVG_MS FROM V$SQL WHERE SQL_ID = 'your_sql_id' AND PLAN_HASH_VALUE = 123456789;
- 若存在,执行导入并固定:
DECLARE
l_plans NUMBER;
BEGIN
l_plans := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
sql_id => 'your_sql_id',
plan_hash_value => 123456789
);
DBMS_OUTPUT.PUT_LINE('Loaded ' || l_plans || ' plans');
END;
- 然后立刻设为
FIXED = YES:
UPDATE SQL_PLAN_BASELINES SET ENABLED = 'YES', FIXED = 'YES' WHERE SQL_HANDLE = (SELECT SQL_HANDLE FROM DBA_SQL_PLAN_BASELINES WHERE SQL_TEXT LIKE '%your_keyword%') AND PLAN_NAME LIKE 'SQL_PLAN%123456789%';
注意:FIXED = YES 表示优化器必须用这个计划,哪怕统计信息更新、绑定变量值变化也不许换;但这也意味着你得自己承担“未来数据分布彻底改变后该计划变差”的风险。
为什么不用 DBA_HIST_SQLSTAT 直接看性能?
DBA_HIST_SQLSTAT 里只有聚合指标(如 ELAPSED_TIME_TOTAL、EXECUTIONS_TOTAL),没有执行计划本身。它只能告诉你“这条 SQL 在某个快照里变慢了”,但无法回答“为什么慢”——因为慢可能来自计划突变,也可能来自 I/O 延迟升高、CPU 抢占、锁等待等外部因素。
- 典型误用:只查
DBA_HIST_SQLSTAT发现ELAPSED_TIME_TOTAL翻倍,就认定是计划问题 → 实际可能是那段时间归档日志写满,导致 direct path read 等待飙升 - 正确做法:先用
DBA_HIST_SQL_PLAN确认计划是否变,再用DBA_HIST_ACTIVE_SESS_HISTORY查那段时间的等待事件占比 - 如果
DBA_HIST_SQL_PLAN显示计划没变,但DBA_HIST_SQLSTAT显示耗时翻倍,那基本可以排除执行计划问题,转向系统级排查
最常被忽略的一点:SPM 的 baseline 默认是 ENABLED = YES 但 FIXED = NO,这意味着优化器仍可能选择其他计划(比如发现新索引后生成更优 plan);不显式设 FIXED = YES,等于没锁住。











