Oracle固定执行计划的方法---SQL Plan Baseline

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

八、补充知识

  1. 生产环境推荐手动加载 :不要在生产环境开启 OPTIMIZER_CAPTURE_SQL_PLAN_BASELINES=TRUE,避免为大量 SQL 生成不必要的基线。应针对已知的关键 SQL 手动加载。

  2. 升级前预加载基线:在数据库升级前,将关键 SQL 的当前执行计划加载为基线并标记为 FIXED,升级完成后逐步"解固"并通过 Evolve 验证新计划。

  3. 绑定后刷除旧游标:基线绑定成功后,建议将共享池中的旧游标清除,确保后续执行立即使用新基线:

    sql 复制代码
    SELECT address, hash_value FROM v$sqlarea WHERE sql_id = '目标SQL_ID';
    EXEC DBMS_SHARED_POOL.PURGE('地址,哈希值', 'C');
  4. 基线的存储位置:基线数据存储在 SYSAUX 表空间中,需确保 SYSAUX 有足够的空间。可通过以下 SQL 查看占用情况:

    sql 复制代码
    SELECT occupant_desc, space_usage_kbytes/1024 AS "MB"
    FROM v$sysaux_occupants
    WHERE occupant_name = 'SQL_MANAGEMENT_BASE';
  5. 跨环境迁移基线:可以通过 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;
    /

相关推荐
数据库小学妹1 小时前
数据库高可用演练怎么做?稳态定义、注入点与中止条件
数据库·rto·高可用架构·运维体系·故障演练·容灾切换
zyseo81 小时前
谷歌SEO 站内搜索优化实战:把站内搜索词变成关键词金矿
java·服务器·数据库
qq_401700411 小时前
Qt 串口/网口通信:Hex 与 ASCII 编码转换深度指南
开发语言·数据库·qt
码爸1 小时前
多源异构数据库实时同步至 Doris
数据库·flink·scala
王大傻09281 小时前
堆叠注入(Stacked Queries Injection)详解:原理、利用与防御
服务器·网络·数据库·web安全·网络安全
ClouGence2 小时前
数据库迁移工具 CloudCanal v6.5.0.0 发布:新增 TDSQL PostgreSQL 多条链路,支持 MongoDB 双向同步
数据库·mysql·mongodb
AIGS0012 小时前
从814张表到250个本体:本体建模在做什么
数据库·excel·erp·数据中台·智能问数·本体语义·企业大脑
fengkai45453 小时前
十二、Redis -1
运维·数据库·redis
这个DBA有点耶3 小时前
Change Buffer深入:二级索引写入的隐形加速器与它的代价
数据库·mysql·代码规范