Oracle固定执行计划的方法---SQL Plan Baseline
一、为什么需要 SQL Plan Baseline?
Oracle 数据库中最令人头疼的问题之一,就是 SQL 执行计划突然变差(Plan Regression),明明昨天还运行良好的查询,今天却因为统计信息更新、数据量变化或数据库升级,优化器选择了一个糟糕的执行计划(比如从索引扫描变成全表扫描),导致响应时间从毫秒级暴增到分钟级。
导致执行计划突变的常见原因:
- 统计信息被重新收集(
DBMS_STATS.GATHER_TABLE_STATS) - 数据库版本升级或补丁应用(优化器行为变化)
- 初始化参数变更(如
OPTIMIZER_MODE、OPTIMIZER_INDEX_COST_ADJ) - Schema 对象变更(如新增/删除索引、修改表结构)
- 系统统计信息变化(
DBMS_STATS.GATHER_SYSTEM_STATS) - 数据分布发生显著变化
传统方案的局限性:
| 方案 | 问题 |
|---|---|
| 停止统计信息收集 | 因噎废食,其他 SQL 的性能会因统计信息过时而恶化 |
| 锁定统计信息 | 同上,且维护成本高 |
| 存储大纲(Stored Outlines) | Oracle 11g 起已标记为废弃(Deprecated),刚性固定,无法自适应 |
| SQL Profile | 只能在问题发生后被动修复 |
| SQL Hint | 需要修改 SQL 文本,对于第三方应用(如 EBS)无法实施 |
SQL Plan Management(SPM) 是 Oracle 11g 引入的解决方案,其核心哲学是:
"首先保证性能不倒退,然后谨慎地引入可能更好的新计划。"
Oracle 11g 引入的 SQL Plan Management(SPM) 从根本上解决了这一难题。它既能 主动稳定 执行计划,又保留了 渐进式优化 的可能性------既不会因为新计划的出现而意外退化,也不会阻止优化器在未来使用更优的新计划,而是在两者之间找到了一个优雅的平衡点。
SPM 与其他方案的对比
| 对比维度 | SPM (SQL Plan Baseline) | SQL Profile | Stored Outlines |
|---|---|---|---|
| 引入版本 | Oracle 11g | Oracle 10g | Oracle 9i |
| 是否废弃 | 否(推荐使用) | 否 | 是(11g 起废弃) |
| 是否需要改 SQL | 不需要 | 不需要 | 不需要 |
| 计划数量 | 支持多计划共存 | 单计划 | 单计划 |
| 自动演进 | 支持(Evolve) | 不支持 | 不支持 |
| 灵活度 | 高(可启用/禁用/固定/演进) | 中 | 低(刚性固定) |
| 适用场景 | 长期计划稳定、升级保障 | 紧急修复特定 SQL | 遗留系统兼容 |
二、概念与架构
2.1 三个关键术语
| 术语 | 含义 |
|---|---|
| Plan History(计划历史) | 存储优化器为某条 SQL 生成的所有执行计划(包括已接受和未接受的) |
| SQL Plan Baseline(计划基线) | 计划历史中已接受(Accepted) 的计划集合,优化器只会从中选择执行计划 |
| SQL Management Base(SMB) | 存储在 SYSAUX 表空间中的底层存储结构,包含计划历史和基线数据 |
2.2 两个关键初始化参数
| 参数 | 默认值 | 作用 |
|---|---|---|
OPTIMIZER_CAPTURE_SQL_PLAN_BASELINES |
FALSE |
控制是否自动捕获执行计划到基线中 |
OPTIMIZER_USE_SQL_PLAN_BASELINES |
TRUE |
控制优化器在生成执行计划时是否使用已有的基线 |
重要说明 :
OPTIMIZER_USE_SQL_PLAN_BASELINES默认就是TRUE,也就是说 SPM 的"使用"功能默认就是开启的。只是因为没有基线数据,所以不会产生实际效果。
2.3 基线中每条计划的四个关键属性
存储在 DBA_SQL_PLAN_BASELINES 视图中的每条计划,都有以下关键属性:
| 属性 | 取值 | 含义 |
|---|---|---|
| ENABLED | YES/NO | 该计划是否启用。NO 则优化器不会考虑它 |
| ACCEPTED | YES/NO | 该计划是否被接受。只有 ACCEPTED=YES 的计划才会被优化器选用 |
| FIXED | YES/NO | 该计划是否被固定。FIXED=YES 的计划优先级最高,优化器只会从 FIXED 的计划中选择 |
| ORIGIN | AUTO-CAPTURE / MANUAL-LOAD | 计划的来源:自动捕获还是手动加载 |
优先级规则:
- 如果存在
FIXED=YES的计划,优化器只从 FIXED 的计划中选择成本最低的那个 - 如果没有 FIXED 的计划,优化器从
ACCEPTED=YES的计划中选择成本最低的 - 优化器仍然会生成新计划,但新计划会被加入计划历史并标记为
ACCEPTED=NO,不会被使用,除非经过验证(Evolve)
三、捕获执行计划到基线
捕获基线有两种方式:自动捕获 和手动加载。
3.1 自动捕获
自动捕获的原理是:当一条 SQL 首次被编译 时,优化器将其记录到语句日志中;当该 SQL 再次被执行(即被识别为"可重复执行的 SQL")时,优化器自动将当前执行计划作为第一条基线保存。
sql
-- 开启自动捕获
ALTER SESSION SET optimizer_capture_sql_plan_baselines = TRUE;
-- 第一次执行:记录到语句日志(不会生成基线)
select count(*) from t1;
-- 第二次执行:自动捕获基线
select count(*) from t1;
-- 关闭自动捕获(用完即关,避免对所有 SQL 产生基线)
ALTER SESSION SET optimizer_capture_sql_plan_baselines = FALSE;
验证基线是否生成:
sql
set linesize 200 pagesize 999
col sql_handle format a20
col PLAN_NAME format a20
col origin format a14
col sql_text format a30
SELECT sql_handle, plan_name, origin, enabled, accepted, fixed, sql_text
FROM dba_sql_plan_baselines
WHERE sql_text LIKE '%t1%';
SQL_HANDLE PLAN_NAME ORIGIN ENA ACC FIX SQL_TEXT
-------------------- -------------------- -------------- --- --- --- ------------------------------
SQL_e208a16bb98b6a04 SQL_PLAN_f4251dfwsqu AUTO-CAPTURE YES YES NO select count(*) from t1
h4616acf47
注意:自动捕获会对所有重复执行的 SQL 都生成基线,在生产环境中需谨慎使用,建议仅在特定维护窗口内开启。
3.2 手动加载(生产环境推荐)
手动加载是生产环境中最常用的方式,可以从**共享池(Cursor Cache)**或 **SQL Tuning Set(STS)**中加载。
从共享池加载(推荐):
sql
-- dbms_spm.LOAD_PLANS_FROM_CURSOR_CACHE 函数参数
FUNCTION LOAD_PLANS_FROM_CURSOR_CACHE RETURNS BINARY_INTEGER
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SQL_ID VARCHAR2 IN
PLAN_HASH_VALUE NUMBER IN DEFAULT
SQL_HANDLE VARCHAR2 IN
FIXED VARCHAR2 IN DEFAULT
ENABLED VARCHAR2 IN DEFAULT
-- 查询目标 SQL 的 SQL_ID 和 PLAN_HASH_VALUE
SELECT sql_id, plan_hash_value, sql_text
FROM v$sql
WHERE sql_text LIKE '%你的SQL关键字%';
-- 加载到基线
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
sql_id => '你的SQL_ID',
plan_hash_value => 你的PLAN_HASH_VALUE
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已加载');
END;
/
备注:
可以指定sql_handle 将计划关联到已有句柄
从 AWR 历史加载(当计划已不在共享池中时):
sql
-- 12.2 及以上版本可直接从 AWR 加载
-- dbms_spm.LOAD_PLANS_FROM_AWR 函数参数
FUNCTION LOAD_PLANS_FROM_AWR RETURNS BINARY_INTEGER
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
BEGIN_SNAP NUMBER IN
END_SNAP NUMBER IN
BASIC_FILTER VARCHAR2 IN DEFAULT
FIXED VARCHAR2 IN DEFAULT
ENABLED VARCHAR2 IN DEFAULT
COMMIT_ROWS NUMBER IN DEFAULT
DBID NUMBER IN DEFAULT
-- 加载方法
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.LOAD_PLANS_FROM_AWR(
begin_snap => 开始SNAP_ID,
end_snap => 结束SNAP_ID,
basic_filter => q'# sql_id='你的SQL_ID' and plan_hash_value='你的PLAN_HASH_VALUE' #'
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已加载');
END;
/
从 SQL Tuning Set 加载:
sql
-- dbms_spm.LOAD_PLANS_FROM_SQLSET 函数参数
FUNCTION LOAD_PLANS_FROM_SQLSET RETURNS BINARY_INTEGER
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SQLSET_NAME VARCHAR2 IN
SQLSET_OWNER VARCHAR2 IN DEFAULT
BASIC_FILTER VARCHAR2 IN DEFAULT
FIXED VARCHAR2 IN DEFAULT
ENABLED VARCHAR2 IN DEFAULT
COMMIT_ROWS NUMBER IN DEFAULT
-- 加载方法
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.LOAD_PLANS_FROM_SQLSET(
sqlset_name => '你的STS名称',
basic_filter => q'# plan_hash_value = '你的PLAN_HASH_VALUE' #'
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已加载');
END;
/
手动加载的计划,其 ORIGIN 会显示为 MANUAL-LOAD,且 ENABLED 和 ACCEPTED 默认为 YES。
四、查看与管理基线
4.1 查看基线信息
sql
-- 查看基线概要
set linesize 200 pagesize 999
col sql_handle format a20
col PLAN_NAME format a20
col origin format a14
col sql_text format a30
SELECT sql_handle,
plan_name,
origin,
enabled,
accepted,
fixed,
TO_CHAR(created, 'YYYY-MM-DD HH24:MI:SS') AS created,
TO_CHAR(last_modified, 'YYYY-MM-DD HH24:MI:SS') AS last_modified,
executions,
elapsed_time/1000000 AS elapsed_sec
FROM dba_sql_plan_baselines
WHERE sql_text LIKE '%你的SQL关键字%'
ORDER BY sql_handle, plan_name;
-- 查看基线对应的执行计划详情
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => 'SQL_PLAN_xxxxxxxxxxxxxxxx',
format => 'ALL'
)
);
4.2 验证基线是否生效
当基线生效时,执行计划的输出末尾会出现 Note 段提示:
Note
-----
- SQL plan baseline "SQL_PLAN_xxxxxxxxxxxxxxxx" used for this statement
如果没有出现这个 Note,说明基线未被使用。
4.3 修改基线属性
sql
-- 禁用某条基线计划
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => 'SQL_PLAN_xxxxxxxxxxxxxxxx',
attribute_name => 'ENABLED',
attribute_value => 'NO'
);
END;
/
-- 启用某条基线计划
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => 'SQL_PLAN_xxxxxxxxxxxxxxxx',
attribute_name => 'ENABLED',
attribute_value => 'YES'
);
END;
/
4.4 删除基线
sql
-- 删除指定计划
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.DROP_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => 'SQL_PLAN_xxxxxxxxxxxxxxxx'
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已删除');
END;
/
-- 删除某条 SQL 的所有基线(plan_name 传 NULL)
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.DROP_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => NULL
);
END;
/
五、计划演进(Evolve)------让新计划"转正"
当优化器为某条 SQL 生成了新计划时,新计划会被存入计划历史,但 ACCEPTED=NO,不会被使用。如果 DBA 认为新计划更好,可以通过 Evolve 将其"转正"为 ACCEPTED=YES。
5.1 自动演进
sql
-- 开启自动演进任务(12c+)
PROCEDURE CONFIGURE
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
PARAMETER_NAME VARCHAR2 IN
PARAMETER_VALUE VARCHAR2 IN DEFAULT
ALLOW BOOLEAN IN DEFAULT
-- 案例
BEGIN
DBMS_SPM.CONFIGURE('AUTO_EVOLVE_TASK', 'ON');
END;
/
5.2 手动演进
sql
-- 函数参数如下(11g)
FUNCTION EVOLVE_SQL_PLAN_BASELINE RETURNS CLOB
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SQL_HANDLE VARCHAR2 IN DEFAULT
PLAN_NAME VARCHAR2 IN DEFAULT
TIME_LIMIT NUMBER(38) IN DEFAULT
VERIFY VARCHAR2 IN DEFAULT
COMMIT VARCHAR2 IN DEFAULT
-- 演进指定 SQL 的所有未接受计划(带性能验证)
DECLARE
l_report CLOB;
BEGIN
l_report := DBMS_SPM.EVOLVE_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
verify => 'YES', -- YES=验证性能后再决定是否接受
commit => 'YES' -- YES=验证通过后自动接受
);
DBMS_OUTPUT.PUT_LINE(l_report);
END;
/
-- 不验证性能,直接接受(强制转正)
DECLARE
l_report CLOB;
BEGIN
l_report := DBMS_SPM.EVOLVE_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => 'SQL_PLAN_xxxxxxxxxxxxxxxx',
verify => 'NO', -- 不验证,直接接受
commit => 'YES'
);
END;
/
演进报告解读:
------------------------------------------------------
Evolve SQL Plan Baseline Report
------------------------------------------------------
Plan: SYS_SQL_PLAN_xxxx
----------------------
Plan was verified: Time used 41.06 seconds.
Failed performance criterion: Compound improvement ratio < .36
Baseline Plan Test Plan Improv. Ratio
------------- --------- -------------
Execution Status: COMPLETE COMPLETE
Rows Processed: 0 0
Elapsed Time(ms): 5036 1033 4.88
Buffer Gets: 1728 43945 .04
Report Summary
--------------
Number of SQL plan baselines verified: 1.
Number of SQL plan baselines evolved: 0.
报告中 Improvement Ratio < 1 说明新计划性能更差,因此被拒绝。只有 Improvement Ratio > 1 时,新计划才会被接受。
六、固定执行计划(Fixed Baseline)
当需要将某条 SQL 的执行计划彻底锁定 ,不允许优化器做任何改变时,可以将基线标记为 FIXED=YES。
FIXED 的行为规则:
- 如果某条 SQL 有
FIXED=YES的基线计划,优化器只从 FIXED 的计划中选择 - 如果有多个 FIXED 的计划,优化器选择成本最低的那个
- 即使优化器生成了成本更低的新计划,也不会被使用(除非新计划也被标记为 FIXED)
FIXED=YES的计划会自动被视为ACCEPTED=YES
sql
-- 将指定计划标记为 FIXED
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => 'SQL_PLAN_xxxxxxxxxxxxxxxx',
attribute_name => 'FIXED',
attribute_value => 'YES'
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已修改');
END;
/
-- 取消 FIXED 标记
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
sql_handle => 'SQL_xxxxxxxxxxxxxxxx',
plan_name => 'SQL_PLAN_xxxxxxxxxxxxxxxx',
attribute_name => 'FIXED',
attribute_value => 'NO'
);
END;
/
七、案例(用 SPM 固定执行计划)
下面通过一个完整的案例,演示用 SPM 固定执行计划的全过程。
7.1 环境准备
sql
-- 建表
CREATE TABLE orders (
order_id NUMBER PRIMARY KEY,
customer_id NUMBER,
order_date DATE,
amount NUMBER(10,2),
status VARCHAR2(20)
);
-- 插入 100 万行测试数据
-- MOD(LEVEL, 100),取模100,该列的唯一值个数为100个,100万行数据,该列只有100个唯一值,对该表过滤,会选择全表扫描
INSERT INTO orders
SELECT LEVEL,
MOD(LEVEL, 100),
SYSDATE - MOD(LEVEL, 365),
DBMS_RANDOM.VALUE(10, 10000),
CASE MOD(LEVEL, 5) WHEN 0 THEN 'COMPLETED' ELSE 'PENDING' END
FROM DUAL
CONNECT BY LEVEL <= 1000000;
COMMIT;
-- 创建索引
CREATE INDEX idx_customer_id ON orders(customer_id);
CREATE INDEX idx_order_date ON orders(order_date);
-- 收集统计信息
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ORDERS');
7.2 模拟查询,确认执行计划为全表扫描
sql
-- 目标 SQL(使用绑定变量)
VARIABLE v_customer NUMBER;
EXEC :v_customer := 100;
SELECT /*+ spm_test */ order_id, order_date, amount, status
FROM orders
WHERE customer_id = :v_customer
ORDER BY order_date DESC;
-- 获取其 SQL_ID 和计划哈希值
SELECT sql_text,SQL_ID, PLAN_HASH_VALUE, EXECUTIONS, ELAPSED_TIME
FROM V$SQL
WHERE SQL_TEXT LIKE '%spm_test%'
AND SQL_TEXT NOT LIKE '%V$SQL%';
SQL_TEXT SQL_ID PLAN_HASH_VALUE EXECUTIONS ELAPSED_TIME
------------------------------ ------------- --------------- ---------- ------------
SELECT /*+ spm_test */ order_i 5pcs01brxdzcb 36248429 1 20582
d, order_date, amount, status
FROM orders WHERE customer_id
= :v_customer ORDER BY order_d
ate DESC
-- 查看执行计划
select * from table(dbms_xplan.display_cursor('5pcs01brxdzcb','',''));
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------
SQL_ID 5pcs01brxdzcb, child number 0
-------------------------------------
SELECT /*+ spm_test */ order_id, order_date, amount, status FROM orders
WHERE customer_id = :v_customer ORDER BY order_date DESC
Plan hash value: 36248429
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 1306 (100)| |
| 1 | SORT ORDER BY | | 9899 | 280K| 1306 (1)| 00:00:01 |
|* 2 | TABLE ACCESS FULL| ORDERS | 9899 | 280K| 1305 (1)| 00:00:01 |
-----------------------------------------------------------------------------
-- 可以看到,执行计划为TABLE ACCESS FULL全表扫描
SQL_ID = '5pcs01brxdzcb',PLAN_HASH_VALUE = 36248429
7.3 使用 SPM 固定到期望的执行计划
第一步:强制生成期望的索引扫描计划
通过添加 Hint 让优化器生成索引扫描计划:
sql
-- 用 Hint 强制走索引扫描,获得我们期望的执行计划
SELECT /*+ spm_test INDEX(orders idx_customer_id) */
order_id, order_date, amount, status
FROM orders
WHERE customer_id = 100
ORDER BY order_date DESC;
-- 查询该 SQL 的 SQL_ID 和 PLAN_HASH_VALUE
SELECT sql_text,SQL_ID, PLAN_HASH_VALUE, EXECUTIONS, ELAPSED_TIME
FROM V$SQL
WHERE SQL_TEXT LIKE '%spm_test%'
AND SQL_TEXT NOT LIKE '%V$SQL%';
SQL_TEXT SQL_ID PLAN_HASH_VALUE EXECUTIONS ELAPSED_TIME
------------------------------ ------------- --------------- ---------- ------------
SELECT /*+ spm_test INDEX(orde 8v9wtt7hznq5c 1479940487 1 2183
rs idx_customer_id) */
order_id, order_date, amount,
status FROM orders WHERE custo
mer_id = 100 ORDER BY order_da
te DESC
-- 查看执行计划
select * from table(dbms_xplan.display_cursor('8v9wtt7hznq5c','',''));
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------
SQL_ID 8v9wtt7hznq5c, child number 0
-------------------------------------
SELECT /*+ spm_test INDEX(orders idx_customer_id) */ order_id,
order_date, amount, status FROM orders WHERE customer_id = 100 ORDER BY
order_date DESC
Plan hash value: 1479940487
--------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 4715 (100)| |
| 1 | SORT ORDER BY | | 9899 | 280K| 4715 (1)| 00:00:01 |
| 2 | TABLE ACCESS BY INDEX ROWID BATCHED| ORDERS | 9899 | 280K| 4714 (1)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | IDX_CUSTOMER_ID | 9899 | | 22 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------------
-- 确认执行计划INDEX RANGE SCAN是我们期望固定的
- 带 Hint 的 SQL:
SQL_ID = '8v9wtt7hznq5c',PLAN_HASH_VALUE = 1479940487
第二步:获取原始 SQL(不带 Hint)的 SQL_ID
sql
-- 查询原始 SQL 的 SQL_ID
SELECT sql_text,SQL_ID, PLAN_HASH_VALUE, EXECUTIONS, ELAPSED_TIME
FROM V$SQL
WHERE SQL_TEXT LIKE '%spm_test%'
AND SQL_TEXT NOT LIKE '%V$SQL%';
- 原始 SQL:
SQL_ID = '5pcs01brxdzcb',PLAN_HASH_VALUE = 36248429(全表扫描)
第三步:将原始 SQL 加载到基线
sql
-- 先将原始 SQL 的当前计划加载到基线中
set serveroutput on
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
sql_id => '5pcs01brxdzcb',
plan_hash_value => 36248429
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已加载');
END;
/
第四步:查询生成的 SQL_HANDLE
sql
SELECT sql_handle, plan_name, origin, enabled, accepted, fixed, sql_text
FROM dba_sql_plan_baselines
WHERE sql_text LIKE 'SELECT object_name FROM spm_test%';
SQL_HANDLE PLAN_NAME ORIGIN ENA ACC FIX SQL_TEXT
-------------------- -------------------- -------------- --- --- --- ------------------------------
SQL_2387226b3f42e915 SQL_PLAN_271t2dczn5u MANUAL-LOAD-FR YES YES NO SELECT /*+ spm_test */ order_i
8p7332c4b8 OM-CURSOR-CACH d, order_date, amount, status
E FROM orders
WHERE cu
得到:
SQL_HANDLE = 'SQL_2387226b3f42e915'PLAN_NAME = 'SQL_PLAN_271t2dczn5u8p7332c4b8'
第五步:将带 Hint 的好计划关联到原始 SQL 的基线上
这是最关键的一步------将带 Hint 的 SQL 的执行计划,"嫁接"到原始 SQL 的基线上:
sql
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
sql_id => '8v9wtt7hznq5c', -- 带 Hint 的 SQL 的 SQL_ID
plan_hash_value => 1479940487, -- 带 Hint 的 SQL 的 PLAN_HASH_VALUE
sql_handle => 'SQL_2387226b3f42e915' -- 原始 SQL 的 SQL_HANDLE
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已加载');
END;
/
第六步:删除旧的全表扫描计划(可选)
sql
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.DROP_SQL_PLAN_BASELINE(
sql_handle => 'SQL_a1b2c3d4e5f6g7h8',
plan_name => 'SQL_PLAN_271t2dczn5u8p7332c4b8' -- 全表扫描的计划
);
DBMS_OUTPUT.PUT_LINE( l_plans || ' 个计划已删除');
END;
/
第七步:将好的计划标记为 FIXED
sql
-- 先查询新加载的计划的 PLAN_NAME
SELECT plan_name, enabled, accepted, fixed
FROM dba_sql_plan_baselines
WHERE sql_handle = 'SQL_a1b2c3d4e5f6g7h8';
SQL_HANDLE PLAN_NAME ORIGIN ENA ACC FIX SQL_TEXT
-------------------- -------------------- -------------- --- --- --- ------------------------------
SQL_2387226b3f42e915 SQL_PLAN_271t2dczn5u MANUAL-LOAD-FR YES YES NO SELECT /*+ spm_test */ order_i
8p7332c4b8 OM-CURSOR-CACH d, order_date, amount, status
E FROM orders
WHERE cu
SQL_2387226b3f42e915 SQL_PLAN_271t2dczn5u MANUAL-LOAD-FR YES YES NO SELECT /*+ spm_test */ order_i
8pabf2c769 OM-CURSOR-CACH d, order_date, amount, status
E FROM orders
WHERE cu
-- SQL_PLAN_271t2dczn5u8pabf2c769这个是我们想要的执行计划
-- 标记为 FIXED
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
sql_handle => 'SQL_2387226b3f42e915',
plan_name => 'SQL_PLAN_271t2dczn5u8pabf2c769', -- 索引扫描的计划
attribute_name => 'FIXED',
attribute_value => 'YES'
);
DBMS_OUTPUT.PUT_LINE(l_plans || ' 个计划已修改');
END;
/
第八步:验证效果
sql
-- 清除共享池中的旧游标,强制重新解析
SELECT address, hash_value FROM v$sqlarea
WHERE sql_id = '5pcs01brxdzcb';
EXEC DBMS_SHARED_POOL.PURGE('地址,哈希值', 'C');
EXEC DBMS_SHARED_POOL.PURGE('00000000613E88F0,4023844235', 'C');
-- 重新执行原始 SQL(不带任何 Hint)
VARIABLE v_customer NUMBER;
EXEC :v_customer := 100;
SELECT /*+ spm_test */ order_id, order_date, amount, status
FROM orders
WHERE customer_id = :v_customer
ORDER BY order_date DESC;
-- 验证执行计划
select * from table(dbms_xplan.display_cursor('5pcs01brxdzcb','',''));
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------
SQL_ID 5pcs01brxdzcb, child number 0
-------------------------------------
SELECT /*+ spm_test */ order_id, order_date, amount, status FROM orders
WHERE customer_id = :v_customer ORDER BY order_date DESC
Plan hash value: 1479940487
--------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 4715 (100)| |
| 1 | SORT ORDER BY | | 9899 | 280K| 4715 (1)| 00:00:01 |
| 2 | TABLE ACCESS BY INDEX ROWID BATCHED| ORDERS | 9899 | 280K| 4714 (1)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | IDX_CUSTOMER_ID | 9899 | | 22 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("CUSTOMER_ID"=:V_CUSTOMER)
Note
-----
- SQL plan baseline SQL_PLAN_271t2dczn5u8pabf2c769 used for this statement
-- 监控基线使用情况,也可以看到fixed的基线计划,确实被使用!!
SELECT SQL_HANDLE, PLAN_NAME, ACCEPTED, FIXED,LAST_EXECUTED, EXECUTIONS, ELAPSED_TIME
FROM DBA_SQL_PLAN_BASELINES
WHERE SQL_HANDLE = 'SQL_2387226b3f42e915';
-- 查询结果
SQL_2387226b3f42e915 SQL_PLAN_271t2dczn5u8pabf2c769 YES YES 17-AUG-26 12.14.39.000000 PM 1 2080
此时计划应显示使用了索引扫描,并且
Note部分会注明:(使用了SQL plan baseline)
- SQL plan baseline SQL_PLAN_271t2dczn5u8pabf2c769 used for this statement
八、补充知识
-
生产环境推荐手动加载 :不要在生产环境开启
OPTIMIZER_CAPTURE_SQL_PLAN_BASELINES=TRUE,避免为大量 SQL 生成不必要的基线。应针对已知的关键 SQL 手动加载。 -
升级前预加载基线:在数据库升级前,将关键 SQL 的当前执行计划加载为基线并标记为 FIXED,升级完成后逐步"解固"并通过 Evolve 验证新计划。
-
绑定后刷除旧游标:基线绑定成功后,建议将共享池中的旧游标清除,确保后续执行立即使用新基线:
sqlSELECT address, hash_value FROM v$sqlarea WHERE sql_id = '目标SQL_ID'; EXEC DBMS_SHARED_POOL.PURGE('地址,哈希值', 'C'); -
基线的存储位置:基线数据存储在 SYSAUX 表空间中,需确保 SYSAUX 有足够的空间。可通过以下 SQL 查看占用情况:
sqlSELECT occupant_desc, space_usage_kbytes/1024 AS "MB" FROM v$sysaux_occupants WHERE occupant_name = 'SQL_MANAGEMENT_BASE'; -
跨环境迁移基线:可以通过 Staging Table 将基线从开发环境导出并导入到生产环境:
sql-- 创建 Staging 表 EXEC DBMS_SPM.CREATE_STGTAB_BASELINE(table_name => 'SPM_STAGING'); -- 打包基线到 Staging 表 DECLARE l_plans PLS_INTEGER; BEGIN l_plans := DBMS_SPM.PACK_STGTAB_BASELINE( table_name => 'SPM_STAGING', sql_handle => 'SQL_xxxxxxxxxxxxxxxx' ); END; / -- 在目标环境解包 DECLARE l_plans PLS_INTEGER; BEGIN l_plans := DBMS_SPM.UNPACK_STGTAB_BASELINE( table_name => 'SPM_STAGING' ); END; /