做过Oracle数据库优化的朋友都知道,执行计划飘忽不定是让人最头疼的问题之一。想象一下这样的场景:一条关键业务SQL昨天还跑得好好的,今天突然就变慢了,检查发现执行计划完全变了样。这种情况在OLTP系统中尤为常见,往往会导致业务高峰期出现性能雪崩。
我遇到过最夸张的一个案例是某电商平台的订单查询SQL,原本走索引只需要几十毫秒,执行计划突变后改为全表扫描,直接拖垮了整个数据库。当时凌晨三点被叫起来处理,那种酸爽至今难忘。
SQL Profile的本质就是给SQL语句"打补丁",在不修改原SQL文本的情况下,通过Hint来引导优化器生成我们想要的执行计划。与SQL Patch不同,SQL Profile是通过DBMS_SQLTUNE包来创建和管理的,属于Oracle官方推荐的执行计划绑定方式。
这个方法的精髓在于直接从内存中提取好的执行计划对应的Hint:
sql复制select extractvalue(value(d), '/hint')
from xmltable('/*/outline_data/hint'
passing (select xmltype(other_xml)
from v$sql_plan
where sql_id = '&good_sqlid')) d;
我曾经在客户严格管控的内网环境用过这个方法,当时连脚本都无法上传,只能手敲这几十行代码。虽然看起来简陋,但效果一点不打折扣,就像瑞士军刀一样小巧但实用。
这个由Oracle ACE总监Carlos Sierra编写的脚本,实际上做了三件事:
它的独特价值在于支持跨数据库迁移执行计划。我曾在数据仓库迁移项目中使用它,将测试环境调优好的执行计划批量应用到生产环境,节省了大量重复调优时间。
这个脚本是前者的升级版,最大的改进是交互式操作:
sql复制SQL> @coe_load_sql_profile
Enter original_sql_id: 18z48fk2jy688 -- 问题SQL
Enter modified_sql_id: 189a578w09x47 -- 优化后的SQL
Enter plan_hash_value: 4244861920 -- 好的执行计划
它自动完成好坏SQL的Hint提取和替换,就像有个专家在旁边手把手指导。实测下来,用这个脚本绑定一个执行计划平均只需2分钟,特别适合紧急故障处理。
我们先创建一个经典的两表关联查询:
sql复制-- 创建测试表
CREATE TABLE t1 AS SELECT * FROM dba_objects;
CREATE TABLE t2 AS SELECT * FROM dba_objects;
-- 创建问题索引
CREATE INDEX t2_idx ON t2(object_id);
-- 收集统计信息
EXEC dbms_stats.gather_table_stats(user, 'T1');
EXEC dbms_stats.gather_table_stats(user, 'T2');
然后故意制造一个性能问题:
sql复制-- 好的SQL:走索引
SELECT * FROM t2 WHERE object_id = 100;
-- 坏的SQL:强制全表扫描
SELECT /*+ FULL(t2) */ * FROM t2 WHERE object_id = 100;
查询两个SQL的ID和执行计划:
sql复制SELECT sql_id, plan_hash_value, sql_text
FROM v$sql
WHERE sql_text LIKE '%t2 WHERE object_id = 100%';
假设得到如下结果:
执行绑定:
sql复制@coe_load_sql_profile
-- 依次输入坏SQL ID、好SQL ID、好执行计划的HASH值
验证效果:
sql复制-- 再次执行原问题SQL
SELECT /*+ FULL(t2) */ * FROM t2 WHERE object_id = 100;
-- 查看执行计划
SELECT * FROM TABLE(dbms_xplan.display_cursor('a12bc34d56efg',null,'ALL'));
你会看到执行计划已经从全表扫描变为索引扫描,同时Note部分显示SQL Profile已生效。
Hint不生效:确保Hint包含Query Block Name,例如INDEX(@"SEL$1" "T2"@"SEL$1")而不是简单的INDEX(T2)
统计信息干扰:手动绑定后,建议锁定统计信息:
sql复制EXEC dbms_stats.lock_table_stats('SCOTT','T2');
环境差异:测试环境绑定的Profile迁移到生产环境时,要检查索引是否一致
当需要处理大量SQL时,可以用这个PL/SQL块自动生成绑定脚本:
sql复制DECLARE
CURSOR c_bad_sql IS
SELECT sql_id, plan_hash_value
FROM v$sql
WHERE executions > 100
AND buffer_gets/executions > 10000;
BEGIN
FOR r IN c_bad_sql LOOP
DBMS_OUTPUT.PUT_LINE('@coe_load_sql_profile '||r.sql_id||' [good_sql_id] '||r.plan_hash_value);
END LOOP;
END;
建议创建定期检查Job:
sql复制BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'CHECK_SQL_PROFILES',
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN
FOR c IN (SELECT name FROM dba_sql_profiles) LOOP
-- 检查Profile是否生效的逻辑
END LOOP;
END;',
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=DAILY;BYHOUR=2',
enabled => TRUE);
END;
在Oracle 11g之后,可以形成防御体系:
我用100万数据量的测试表做了组对比实验:
| 方法 | 绑定耗时 | 执行计划稳定性 | 对统计信息敏感度 |
|---|---|---|---|
| 极简版 | 1分钟 | 高 | 低 |
| coe_xfr_sql_profile | 5分钟 | 极高 | 中 |
| coe_load_sql_profile | 2分钟 | 高 | 低 |
其中coe_xfr_sql_profile在跨实例迁移时稳定性最好,而coe_load_sql_profile在紧急处理时效率最高。
问题1:绑定后执行计划没变化?
问题2:绑定后性能反而下降?
sql复制EXEC dbms_sqltune.drop_sql_profile('profile_name');
问题3:生产环境不敢直接操作?
去年某证券交易系统升级后,关键的风控查询SQL执行时间从200ms暴涨到15秒。通过分析发现:
整个过程只用了30分钟就解决了问题,比回退版本或修改SQL的风险小得多。这个案例让我深刻体会到手动绑定执行计划的价值。