为何Oracle执行后的DDL语句(如CTAS)未显示在V$SQL视图中?如何获取其SQL_ID用于SQL计划基线?
一、为什么V$SQL看不到CTAS这类DDL?
V$SQL视图主要存储的是可以被共享复用的SQL语句——也就是那些执行后会被缓存到共享池、后续相同语句可以直接复用执行计划的SQL(比如SELECT、INSERT、UPDATE这类DML)。
但DDL语句(包括CTAS)有个关键特性:它们被Oracle标记为不可共享(non-sharable)。原因很简单:
- DDL是一次性的元数据变更操作,执行后会直接修改数据字典,几乎没有被重复执行的需求;
- Oracle不会将这类语句缓存到共享池,执行完成后就会从V$SQL中被清理掉(甚至很多时候根本不会被写入V$SQL)。
不过要注意:CTAS里的SELECT子查询部分,其实是会被记录到V$SQL中的,但外层的CREATE TABLE ... AS这个DDL本身不会出现在V$SQL里。
二、如何获取DDL的SQL_ID?
既然V$SQL靠不住,我们可以通过以下几种方式来捕获CTAS这类DDL的SQL_ID:
1. 执行DDL时强制生成监控记录(推荐即时捕获)
给DDL语句加上/*+ MONITOR */提示,强制Oracle生成SQL监控记录,之后就能在V$SQL_MONITOR视图中查到SQL_ID:
/*+ MONITOR */ CREATE TABLE my_new_table AS SELECT * FROM my_source_table WHERE id < 1000;
执行后立即查询:
SELECT sql_id, sql_text FROM v$sql_monitor WHERE sql_text LIKE '%CREATE TABLE my_new_table%' AND status = 'DONE';
如果DDL执行时间很短(比如几秒内),可能需要先设置ALTER SESSION SET SQL_MONITOR_FORCE_TRACING = TRUE;来确保监控记录被生成。
2. 使用DBMS_SQL包执行DDL,直接获取SQL_ID
如果是通过PL/SQL执行DDL,可以用DBMS_SQL包来捕获SQL_ID,执行完就能直接拿到:
SET SERVEROUTPUT ON; DECLARE l_cursor NUMBER; l_sql_id VARCHAR2(13); l_sql_stmt VARCHAR2(1000) := 'CREATE TABLE my_plsql_table AS SELECT * FROM my_source_table'; BEGIN l_cursor := DBMS_SQL.OPEN_CURSOR; DBMS_SQL.PARSE(l_cursor, l_sql_stmt, DBMS_SQL.NATIVE); l_sql_id := DBMS_SQL.LAST_SQL_ID; -- 获取刚解析的SQL_ID DBMS_SQL.EXECUTE(l_cursor); DBMS_SQL.CLOSE_CURSOR(l_cursor); DBMS_OUTPUT.PUT_LINE('DDL的SQL_ID是: ' || l_sql_id); END; /
3. 从AWR历史记录中查询(针对已执行的DDL)
如果DDL已经执行过一段时间,且AWR快照保留了相关记录,可以查询DBA_HIST_SQLTEXT视图:
SELECT sql_id, sql_text FROM dba_hist_sqltext WHERE sql_text LIKE '%CREATE TABLE%AS SELECT%' AND sql_text NOT LIKE '%SELECT%CREATE TABLE%'; -- 过滤掉子查询里的内容
注意:这个方法依赖AWR的快照频率和保留时间,如果DDL执行时间超过AWR保留期,就查不到了。
4. 从V$SESSION中临时捕获(执行过程中)
在DDL执行的过程中(比如大表的CTAS需要跑几分钟),可以查询当前会话的SQL_ID:
SELECT sql_id FROM v$session WHERE sid = <你的会话SID> AND serial# = <你的会话SERIAL#>;
但这个方法只能在DDL执行时拿到,执行完成后会话的sql_id会被清空,所以适合长时间运行的DDL。
三、获取SQL_ID后用于SQL计划基线
拿到SQL_ID后,你可以用DBMS_SPM包来创建或加载SQL计划基线。比如:
-- 从SQL监控记录中加载计划基线 DECLARE l_plans_loaded NUMBER; BEGIN l_plans_loaded := DBMS_SPM.LOAD_PLANS_FROM_SQLSET( sqlset_name => 'MY_SQLSET', basic_filter => 'sql_id = ''你的SQL_ID''' ); DBMS_OUTPUT.PUT_LINE('加载了' || l_plans_loaded || '个计划基线'); END; /
或者如果已经有执行计划的话,也可以直接创建基线:
DECLARE l_baseline_name VARCHAR2(100); BEGIN l_baseline_name := DBMS_SPM.CREATE_SQL_PLAN_BASELINE( sql_id => '你的SQL_ID', plan_hash_value => <计划HASH值> ); DBMS_OUTPUT.PUT_LINE('创建的基线名称: ' || l_baseline_name); END; /
内容的提问来源于stack exchange,提问作者Yaser

