以下の関数は、GV $ SQL_BIND_CAPTUREのデータを使用して、バインド変数を最近のリテラルに置き換えます。 Oracleバインドメタデータは常に利用できるとは限らないため、以下の関数がすべてのクエリで機能するとは限りません。
関数を作成します:
create or replace function get_sql_with_literals(p_sql_id varchar2) return clob authid current_user is
/*
Purpose: Generate a SQL statement with literals, based on values in GV$SQL_BIND_CAPTURE.
This can be helpful for queries with hundreds of bind variables (or cursor sharing),
and you don't want to spend minutes manually typing each variable.
*/
v_sql_text clob;
v_names sys.odcivarchar2list;
v_values sys.odcivarchar2list;
begin
--Get the SQL_ID and text.
--(Use dynamic SQL to simplify privileges. Your user must have access to GV$ views,
-- but you don't need to have them directly granted to your user, role access is fine.)
execute immediate
q'[
select sql_fulltext
from gv$sql
--There may be multiple rows, for clusters or child cursors.
--Can't use distinct with CLOB SQL_FULLTEXT, but since the values will be the same
--we can pick any one of the rows.
where sql_id = :p_sql_id
and rownum = 1
]'
into v_sql_text
using p_sql_id;
--Try to find the binds from GV$SQL_MONITOR. If the values exist, this is the most accurate source.
execute immediate
q'[
--Get the binds for the latest run.
select
case
when name like ':SYS_%' then ':"' || substr(name, 2) || '"'
else name
end name,
case
when dtystr like 'NUMBER%' then nvl(the_value, 'NULL')
when dtystr like 'VARCHAR2%' then '''' || the_value || ''''
when dtystr like 'DATE%' then 'to_date('''||the_value||''', ''MM/DD/YYYY HH24:MI:SS'')'
--From: https://ardentperf.com/2013/11/19/convert-rawhex-to-timestamp/
when dtystr like 'TIMESTAMP%' then
'to_timestamp('''||
to_char( to_number( substr( the_value, 1, 2 ), 'xx' ) - 100, 'fm00' ) ||
to_char( to_number( substr( the_value, 3, 2 ), 'xx' ) - 100, 'fm00' ) ||
to_char( to_number( substr( the_value, 5, 2 ), 'xx' ), 'fm00' ) ||
to_char( to_number( substr( the_value, 7, 2 ), 'xx' ), 'fm00' ) ||
to_char( to_number( substr( the_value, 9, 2 ), 'xx' )-1, 'fm00' ) ||
to_char( to_number( substr( the_value,11, 2 ), 'xx' )-1, 'fm00' ) ||
to_char( to_number( substr( the_value,13, 2 ), 'xx' )-1, 'fm00' ) ||
''', ''yyyymmddhh24miss'')'
else 'Unknown type: '||dtystr
end the_value
from
(
select xmltype.createXML(binds_xml) binds_xml
from
(
select binds_xml, last_refresh_time, max(last_refresh_time) over () max_last_refresh_time
from gv$sql_monitor
where sql_id = :p_sql_id
and binds_xml is not null
)
where last_refresh_time = max_last_refresh_time
and rownum = 1
) binds
cross join
xmltable('/binds/bind' passing binds.binds_xml
columns
name varchar2(128) path '@name',
dtystr varchar2(128) path '@dtystr',
the_value varchar2(4000) path '/'
)
--Match longest names first to avoid matching substrings.
--For example, we don't want ":b1" to be matched to ":b10".
order by length(name) desc, the_value
]'
bulk collect into v_names, v_values
using p_sql_id;
--Use gv$sql_bind_capture if there was nothing from SQL Monitor.
if v_names is null or v_names.count = 0 then
--Get bind data.
execute immediate
q'[
select
name,
--Convert to literals that can be plugged in.
case
when datatype_string like 'NUMBER%' then nvl(value_string, 'NULL')
when datatype_string like 'VARCHAR%' then '''' || value_string || ''''
when datatype_string like 'DATE%' then 'to_date('''||value_string||''', ''MM/DD/YYYY HH24:MI:SS'')'
--TODO: Add more types here
end value
from
(
select
datatype_string,
--If CURSOR_SHARING=FORCE, literals are replaced with bind variables and use a different format.
--The name is stored as :SYS_B_01, but the actual string will be :"SYS_B_01".
case
when name like ':SYS_%' then ':"' || substr(name, 2) || '"'
else name
end name,
position,
value_string,
--If there are multiple bind values captured, only get the latest set.
row_number() over (partition by name order by last_captured desc nulls last, address) last_when_1
from gv$sql_bind_capture
where sql_id = :p_sql_id
)
where last_when_1 = 1
--Match longest names first to avoid matching substrings.
--For example, we don't want ":b1" to be matched to ":b10".
order by length(name) desc, position
]'
bulk collect into v_names, v_values
using p_sql_id;
end if;
--Loop through the binds and replace them.
for i in 1 .. v_names.count loop
v_sql_text := replace(v_sql_text, v_names(i), v_values(i));
end loop;
--Return the SQL.
return v_sql_text;
end;
/
関数を実行します:
Oracleは、バインド変数の最初のインスタンスのみをキャプチャします。プロシージャを実行する前にこのステートメントを実行して、既存のバインドデータをクリアします。このステートメントを本番環境で実行する場合は注意が必要です。キャッシュされたプランが失われるため、システムの速度が一時的に低下する可能性があります。
alter system flush shared_pool;
次に、SQL_IDを見つけます。これは、SQLの汎用性または一意性によっては、注意が必要な場合があります。
select *
from gv$sql
where lower(sql_fulltext) like lower('%unique_string%')
and sql_fulltext not like '%quine%';
最後に、SQLをプロシージャにプラグインすると、リテラルを含むコードが返されます。残念ながら、SQLはすべてのフォーマットを失いました。これを回避する簡単な方法はありません。それが大したことなら、代わりにPL / Scopeを使って何かを構築し、代わりにプロシージャ内の変数を置き換えることができるかもしれませんが、途方もなく複雑になると思います。うまくいけば、IDEにコードビューティファイアがあります。
select get_sql_with_literals(p_sql_id => '65xzbdjubzdqz') sql
from dual;
手順を含む完全な例:
ソースコードを変更し、クエリを簡単に見つけられるように一意の識別子を追加しました。解析されたクエリには通常のコメントが含まれていないため、ヒントを使用しました。また、例をより現実的にするために、文字列と日付を含むようにデータ型を変更しました。
drop table test1 purge;
create table test1(col1 number, col2 varchar2(100), col3 date);
create or replace procedure test_procedure is
C_Constant constant date := date '2000-01-01';
v_output1 number;
v_output2 varchar2(100);
v_output3 date;
CURSOR cFunnyCursor (
v1 NUMBER,
v2 VARCHAR2
) IS
SELECT /*+ unique_string_1 */ * FROM TEST1
WHERE col1 = v1
AND col2 != v2
AND col3 = C_CONSTANT;
begin
open cFunnyCursor(3, 'asdf');
fetch cFunnyCursor into v_output1, v_output2, v_output3;
close cFunnyCursor;
end;
/
begin
test_procedure;
end;
/
select *
from gv$sql
where lower(sql_fulltext) like lower('%unique_string%')
and sql_fulltext not like '%quine%';
結果:
select get_sql_with_literals(p_sql_id => '65xzbdjubzdqz') sql
from dual;
SQL
---
SELECT /*+ unique_string_1 */ * FROM TEST1 WHERE COL1 = 3 AND COL2 != 'asdf' AND COL3 = to_date('01/01/2000 00:00:00', 'MM/DD/YYYY HH24:MI:SS')