【问题标题】:PL/SQL - Optional conditions in where-clause - without dynamic sql?PL/SQL - where 子句中的可选条件 - 没有动态 sql?
【发布时间】:2010-12-15 13:16:57
【问题描述】:

我有一个查询,并非所有条件都是必需的。以下是使用所有条件时的示例:

select num
from (select distinct q.num
       from cqqv q
       where q.bcode = '1234567' --this is variable
             and q.lb = 'AXCT' --this is variable
             and q.type = 'privt' --this is variable
             and q.edate > sysdate - 30 --this is variable
       order by dbms_random.value()) subq
where rownum <= 10; --this is variable

标记为--this is variable 的部分是不同的部分!如果未指定条件,则没有默认值。例如,如果输入为 q.type 指定了“*”(但其他所有内容保持不变),那么查询应该匹配所有类型,并执行如下:

select num
from (select distinct q.num
       from cqqv q
       where q.bcode = '1234567' --this is variable
             and q.lb = 'AXCT' --this is variable
             --and q.type = 'privt' --this condition ignored because of "type=*" in input
             and q.edate > sysdate - 30 --this is variable
       order by dbms_random.value()) subq
where rownum <= 10; --this is variable

我知道可以使用动态 sql 即时构建此查询,但我想知道这会导致什么样的性能问题,以及是否有更好的方法来做到这一点。

【问题讨论】:

    标签: oracle plsql oracle10g dynamic-sql


    【解决方案1】:

    虽然你可以这样做......

    select num
    from (select distinct q.num
           from cqqv q
           where 1=1
                 and (:bcode is null or q.bcode = :bcode)
                 and (:lb is null or q.lb = :lb)
                 and (:type is null or q.type = :type)
                 and (:edate is null or q.edate > :edate - 30)
           order by dbms_random.value()) subq
    where rownum <= :numrows
    

    ...使用动态 SQL 的性能通常会更好,因为它会生成更有针对性的查询计划。在上述查询中,Oracle 无法判断是在 bcode 还是 lb 或 type 或 edate 上使用索引,并且可能每次都会执行全表扫描。

    当然,您必须在动态查询中使用绑定变量,而不是将文字值连接到字符串中,否则性能(以及可伸缩性和安全性)将非常糟糕强>.

    要清楚,我想到的动态版本会这样工作:

    declare
        rc sys_refcursor;
        q long;
    begin
        q := 'select num
        from (select distinct q.num
               from cqqv q
               where 1=1';
    
        if p_bcode is not null then
            q := q || 'and q.bcode = :bcode';
        else
            q := q || 'and (1=1 or :bcode is null)';
        end if;
    
        if p_lb is not null then
            q := q || 'and q.lb = :lb';
        else
            q := q || 'and (1=1 or :lb is null)';
        end if;
    
        if p_type is not null then
            q := q || 'and q.type = :type';
        else
            q := q || 'and (1=1 or :type is null)';
        end if;
    
        if p_edate is not null then
            q := q || 'and q.edate = :edate';
        else
            q := q || 'and (1=1 or :edate is null)';
        end if;
    
        q := q || ' order by dbms_random.value()) subq
        where rownum <= :numrows';
    
        open rc for q using p_bcode, p_lb, p_type, p_edate, p_numrows;
        return rc;
    end;
    

    这意味着结果查询是“sargable”(我必须承认这是一个新词!),因为结果查询运行将是(例如):

    select num
    from (select distinct q.num
           from cqqv q
           where 1=1
                 and q.bcode = :bcode
                 and q.lb = :lb
                 and (1=1 or :type is null)
                 and (1=1 or :edate is null)
           order by dbms_random.value()) subq
    where rownum <= :numrows
    

    但是,我同意在此示例中这可能需要多达 16 次硬解析。使用本机动态 SQL 时需要“and :bv is null”子句,但可以通过使用 DBMS_SQL 来避免。

    注意:Michal Pravda 在评论中建议在绑定变量为 null 时使用 (1=1 or :bindvar is null),因为它允许优化器消除该子句。

    【讨论】:

    • 我通常这样做,除非性能问题迫使我以另一种方式这样做。一般来说,我认为这应该是首选的方式,因为总是使用动态 SQL 意味着大量的硬解析、混乱的共享池和可能的其他问题。还有:使程序更长更难阅读。人类程序,除非另有说明。
    • 有趣的是,我查看了如何在动态 SQL 中使用绑定变量(我不知道那个技巧!)。我得试一试。谢谢!
    • 当然,无论我在上面说什么“为空”,我都应该说“='*'”,但你明白了……
    • @ammoQ,确实会有更多使用动态 SQL 的硬解析,但只有一小部分 - 每个 SQL 变体最多一个,即本示例中的 16 个不同查询。与此相比,每次执行全表扫描的成本与能够在动态查询中使用索引的成本相比。如果表很小,查询简单,静态查询可能是最好的。对于更大的数据集和更复杂的查询,我会选择动态版本。
    • 有一种方法可以改进这个解决方案:不要只使用 AND :edate IS NULL 。让优化器在解析/分析阶段开始时将其从查询中消除。这可以通过 AND (1=1 or :edate IS NULL) 来完成。这样,您就可以受益于 OMG Ponies 的上下文变量(在 where 子句中只有“有意义的”条件),而无需它们的复杂性和创建上下文的必要性。在某些情况下,对于长时间运行的查询,完全消除绑定会更高效。但这种情况很少见,如果你不小心,你很容易受到 SQL 注入的影响
    【解决方案2】:

    虽然我同意 Tony 的观点,即使用动态 SQL 的性能更好,但上下文变量是比使用绑定变量更好的方法。

    使用IN_VARIABLE IS NULL OR table.fieldx = IN_VARIABLE 不适合处理可选值。每次提交查询时,Oracle 首先在其共享池中检查该语句之前是否已提交过。如果有,则检索查询的执行计划并执行 SQL。如果在共享池中找不到该语句,Oracle 必须经过解析语句、计算出各种执行路径并提出最佳访问计划(也称为“最佳路径”)的过程才能执行。这个过程被称为“硬解析”,它可能比查询本身花费更长的时间。阅读有关hard/soft parse in Oracle hereAskTom here 的更多信息。

    简而言之-这个:

    and (:bcode is null or q.bcode = :bcode)
    

    ...将执行相同的,动态的或其他的。在动态 SQL 中为可选参数使用绑定变量没有任何好处。该设置仍然会破坏 SARGability...

    上下文参数是 Oracle 9i 中引入的一项功能。它们与包相关联,可用于设置属性值(仅适用于对包具有 EXECUTE 权限的用户,您必须将 CREATE CONTEXT 授予模式)。上下文变量可用于定制动态 SQL,因此它仅包含基于过滤器/搜索条件的查询所需的内容。相比之下,绑定变量(在动态 SQL 中也支持)要求指定一个值,这可能导致搜索查询中的 IN_VARIABLE IS NULL OR table.fieldx = IN_VARIABLE 测试。在实践中,应为每个程序或功能使用单独的上下文变量,以消除价值污染的风险。

    这是您使用上下文变量的查询:

    L_CURSOR SYS_REFCURSOR;
    L_QUERY  VARCHAR2(5000) DEFAULT 'SELECT num
                                       FROM (SELECT DISTINCT q.num
                                               FROM CQQV q
                                              WHERE 1 = 1 ';
    BEGIN
    
        IF IN_BCODE IS NOT NULL THEN
          DBMS_SESSION.SET_CONTEXT('THE_CTX',
                                   'BCODE',
                                   IN_BCODE);
          L_QUERY := L_QUERY || ' AND q.bcode = SYS_CONTEXT(''THE_CTX'', ''BCODE'') ';
        END IF;
    
        IF IN_LB IS NOT NULL THEN
          DBMS_SESSION.SET_CONTEXT('THE_CTX',
                                   'LB',
                                   IN_LB);
          L_QUERY := L_QUERY || ' AND q.lb = SYS_CONTEXT(''THE_CTX'', ''LB'') ';
        END IF;
    
        IF IN_TYPE IS NOT NULL THEN
          DBMS_SESSION.SET_CONTEXT('THE_CTX',
                                   'TYPE',
                                   IN_TYPE);
          L_QUERY := L_QUERY || ' AND q.type = SYS_CONTEXT(''THE_CTX'', ''TYPE'') ';
        END IF;
    
        IF IN_EDATE IS NOT NULL THEN
          DBMS_SESSION.SET_CONTEXT('THE_CTX',
                                   'EDATE',
                                   IN_EDATE);
          L_QUERY := L_QUERY || ' AND q.edate = SYS_CONTEXT(''THE_CTX'', ''EDATE'') - 30 ';
        END IF;
    
        L_QUERY := L_QUERY || ' ORDER BY dbms_random.value()) subq
               WHERE rownum <= :numrows ';
    
        FOR I IN 0 .. (TRUNC(LENGTH(L_QUERY) / 255)) LOOP
          DBMS_OUTPUT.PUT_LINE(SUBSTR(L_QUERY, I * 255 + 1, 255));
        END LOOP;
    
        OPEN L_CURSOR FOR L_QUERY USING IN_ROWNUM;
        RETURN L_CURSOR;
    
    END;
    

    该示例仍然为 rownum 使用绑定变量,因为该值不是可选的。

    DBMS_SESSION.SET_CONTEXT('THE_CTX', 'LB', IN_LB);
    

    SET_CONTEXT 参数如下:

    1. 上下文变量名称。不涉及实例创建
    2. 上下文变量中的变量。上下文变量类似于会话变量,前提是您熟悉 Web 应用程序和会话对象。
    3. 参数 #2 中定义的变量的值。

    绑定与上下文

    绑定变量意味着 Oracle 期望填充变量引用 - 否则会出现 ORA 错误。例如:

    ... L_QUERY USING IN_EXAMPLE_VALUE
    

    ...预计有一个要填充的绑定变量引用。如果IN_EXAMPLE_VALUE 为空,则查询中必须:variable。即:AND :variable IS NULL

    使用上下文变量意味着不必包含无关/冗余逻辑,检查值是否为空。

    重要提示:绑定变量按出现顺序(称为序数)处理,按名称处理。您会注意到USING 子句中没有数据类型声明。序数并不理想 - 如果您在查询中更改它们而不更新 USING 子句,它将中断查询直到它被修复。

    【讨论】:

    • 我还没有听说过上下文变量,我认为它们可能对我正在研究的其他一些东西有用(感谢您引起我的注意!),但我仍然不清楚至于为什么在这种情况下它们比绑定变量更好。举个简单的例子,如果我的查询是: select * from cqqv q where q.bcode = sys_context('ctx1','bcode') 与 select * from cqqv q where q.bcode = :bcode 有什么区别?上下文变量方法是否对绑定变量有影响?
    • 不同之处在于,当您使用绑定变量时,您总是必须在查询中包含绑定变量引用。 IE:如果值为 null,则您必须拥有AND :bcode IS NULL。使用上下文变量意味着不必包含无关/冗余的WHERE 子句
    • 啊哈!我现在看到了!打开游标的代码取决于游标中有多少绑定变量,但是否使用上下文变量无关紧要! :)
    • 嗯,现在我需要与 DBA 交谈以授予我执行 dbms_session.set_context 的权限...
    • @Frustrated:去过那里 - 我提供的信息和两个链接过去对我有用:)
    【解决方案3】:

    我确定的解决方案是生成一个动态 SQL 查询,如下所示:

    select num
    from (select distinct q.NUM
           from cqqv q 
           where  (q.bcode = :bcode) 
                      and  (1=1 or :lb is null) 
                      and  (1=1 or :type is null) 
                      and  (q.edate> :edate) 
                    order by dbms_random.value()) subq 
    where rownum <= :numrows
    

    (在本例中,bcode 和 edate 条件不是可选的,但 lb 和 type 是可选的)

    我认为这是(或非常相似)Michal Pravda 的建议,我们的 DBA 更喜欢这个解决方案而不是上下文变量解决方案。感谢您提供的所有帮助和建议!

    我们的 DBA 找到了详细说明此解决方案的链接:

    Ask Tom: On Popularity and Natural Selection

    【讨论】:

      【解决方案4】:

      我会这样做

      select num
      from (select distinct q.num
             from cqqv q
             where q.bcode = '1234567' --this is variable
                   and q.lb = 'AXCT' --this is variable
                   and q.type = nvl(<variable-type>, q.type)  --this condition ignored because of "type=*" in input
                   and q.edate > sysdate - 30 --this is variable
             order by dbms_random.value()) subq
      where rownum <= 10; --this is variable
      

      当忽略 q.TYPE 过滤时,只需保证 variable-type 为 null。

      【讨论】:

        【解决方案5】:

        其中(columnA = passValue 或passedValue = -1)

        当传入 sql 的值为 -1 时,columnA 可以是任何值..

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-06-14
          • 1970-01-01
          • 1970-01-01
          • 2016-07-29
          • 1970-01-01
          • 1970-01-01
          • 2014-09-17
          • 1970-01-01
          相关资源
          最近更新 更多