PL/SQL 游标的变量/文字替换?

Posted

技术标签:

【中文标题】PL/SQL 游标的变量/文字替换?【英文标题】:Variable/Literal replacement for PL/SQL Cursors? 【发布时间】:2015-12-04 09:16:10 【问题描述】:

我经常需要在 Oracle PL/SQL 中调试游标。我的问题是我最终得到了几百行大光标,其中包含 50 多个变量和常量。我正在寻找一种方法来获取将常量和变量替换为其文字的语句版本。如果我想找出为什么光标没有显示记录/行,我应该最终替换这些变量/文字 30 分钟,然后我才能运行选择并注释掉一些语句以找出问题所在。

如果我有类似的东西

CURSOR cFunnyCursor (
  v1 NUMBER,
  v2 NUMBER
) IS
SELECT * FROM TABLE
WHERE  col1  = v1
AND    col2 != v2
AND    col3  = CONSTANT;

我需要这样的 SELECT:

SELECT * FROM TABLE
WHERE  col1  = 123
AND    col2 != 5324
AND    col3  = 'ValueXyz';

有没有办法以这种方式获取/记录 SELECT,这样我就可以将它复制粘贴到一个新的 SQL 窗口中,这样我就不必花费 30 分钟来替换那些东西? (应该是我可以重用的东西,它没有绑定到那个特殊的游标,因为我经常在大量不同的游标上需要这些东西)。

【问题讨论】:

【参考方案1】:

以下函数使用来自 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;

带有过程的完整示例:

我修改了您的源代码并添加了唯一标识符,以便可以轻松找到查询。我使用了一个提示,因为解析的查询不包括常规 cmets。我还更改了数据类型以包含字符串和日期,以使示例更加真实。

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') 

【讨论】:

好主意,我的 IDE 有一个代码美化器。问题是它创建了一个损坏的选择。我在不属于它们的地方得到了一些变量。 @aLpenbog 一个潜在的问题是替换可能意外替换了子字符串。例如,“:b10”可能已被“:b1”的值替换。我更改了排序以首先匹配最大的字符串。 会有一些性能开销,但这可能是可以容忍的。将您的查询转换为返回引用游标的过程(或函数)。此过程的参数将与当前过程相同。转换现有代码以调用此新过程。现在,您可以在没有周围代码的情况下仅查看查询本身在调试时产生的内容。您还可以在这个新过程中添加一个额外的调试标志(默认为否),允许您在调试时记录参数,但在正常生产运行期间不记录。【参考方案2】:

我这样做的方法是将 sql 复制并粘贴到编辑器窗口中,在所有变量前面加上 :,然后运行查询。当我使用 Toad 时,我得到一个窗口,提示我输入查询中所有绑定变量的值,因此我填写这些值并运行查询。值已保存,因此可以轻松地重新运行查询,或者如果您需要调整值,也可以这样做。

例如:

SELECT * FROM TABLE
WHERE  col1  = v1
AND    col2 != v2
AND    col3  = CONSTANT;

变成

SELECT * FROM TABLE
WHERE  col1  = :v1
AND    col2 != :v2
AND    col3  = :CONSTANT;

【讨论】:

我仍然需要进入例程并将所有这些变量复制/粘贴到提示符中,并将 : 放在每个变量的前面。这并不比用鼠标-> ctfl + f 选择变量名快多少,搜索/替换并放入正确的变量。我需要一些可以代替我的东西。 我同意你关于替换的观点,它不会为你节省太多时间......第一次这样做。但是,如果您必须经常调试这些查询,那么我将保存查询的调试版本(即,所有 : 都已到位),并从上一次运行中记住这些值(无论如何,在 Toad 中) 我的问题是客户因为某些东西不起作用而打电话的情况。我意识到记录/行不在记录集中,我必须找出原因。我们的数据库中有大量非常大的混乱游标,其中包含大量变量。所以我需要 30 分钟来替换变量,并且需要 30 秒来注释掉一些行以找出问题所在。这种情况并不令人满意,所以我需要一个解决方法。【参考方案3】:

我认为您必须使用动态 SQL 功能来获取这些变量值。通过使用 ref 游标变量,您甚至可以看到输出。 请看下面的查询。

DECLARE
vString  VARCHAR2 (32000);
vResult  sys_refcursor;
BEGIN
vString := 
     'SELECT * FROM table   
       WHERE col1 = '|| v1|| ' 
         AND col2 != '|| v2|| ' 
         AND col3 = '|| v;

OPEN vResult FOR vString;

DBMS_OUTPUT.put_line (vString);
END;

如果您有更大的游标查询,这不是一种有效的方法。因为您可能需要将整个 Cursor 查询替换为 Dynamic SQL。

【讨论】:

【参考方案4】:

一种可能的方法是将光标分配给 SYS_REFCURSOR 变量,然后将 SYS_REFCURSOR 分配给绑定变量。

如果您在 Toad 中运行此 sn-p,系统会要求您在弹出窗口中定义 :out 变量:只需选择方向:OUT / 类型:CURSOR,数据集将显示在“数据网格”选项卡。

declare
  l_refcur   sys_refcursor;
  v1         varchar2(4) := 'v1'; 
  v2         varchar2(4) := 'v2'; 
  c_constant varchar2(4) := 'X';
begin
  open l_refcur for
    SELECT * FROM dual
     WHERE dummy  = c_CONSTANT;
  :out := l_refcur;
end;

其他 SQL IDE 也应该支持此功能。

【讨论】:

以上是关于PL/SQL 游标的变量/文字替换?的主要内容,如果未能解决你的问题,请参考以下文章

PL/SQL:游标使用中的 ORA-01001

Oracle笔记4-pl/sql-分支/循环/游标/异常/存储/调用/触发器

orcale 之 PL/SQL的游标

oracle PL/SQL编程语言之游标的使用

oracle PL/SQL编程语言之游标的使用

PL/SQL 打印出存储过程返回的引用游标