【问题标题】:Oracle PL/SQL - Are NO_DATA_FOUND Exceptions bad for stored procedure performance?Oracle PL/SQL - NO_DATA_FOUND 异常对存储过程性能有害吗?
【发布时间】:2010-09-18 07:49:04
【问题描述】:

我正在编写一个需要在其中进行大量调节的存储过程。根据 C#.NET 编码的一般知识,异常会损害性能,我也一直避免在 PL/SQL 中使用它们。我在这个存储过程中的调节主要围绕是否存在记录,我可以通过以下两种方式之一进行:

SELECT COUNT(*) INTO var WHERE condition;
IF var > 0 THEN
   SELECT NEEDED_FIELD INTO otherVar WHERE condition;
....

-或-

SELECT NEEDED_FIELD INTO var WHERE condition;
EXCEPTION
WHEN NO_DATA_FOUND
....

第二种情况对我来说似乎更优雅一些,因为这样我就可以使用 NEEDED_FIELD,我必须在第一种情况下的条件之后的第一个语句中选择它。更少的代码。但是如果使用 COUNT(*) 存储过程会运行得更快,那么我不介意多输入一点来弥补处理速度。

有什么提示吗?我错过了另一种可能性吗?

编辑 我应该提到这一切都已经嵌套在 FOR LOOP 中。不确定这是否对使用游标有影响,因为我认为我不能将游标声明为 FOR LOOP 中的选择。

【问题讨论】:

    标签: sql oracle exception plsql


    【解决方案1】:

    我不会使用显式游标来执行此操作。当可以使用隐式游标时,Steve F. 不再建议人们使用显式游标。

    count(*) 的方法不安全。如果另一个会话删除了count(*)行之后,select ... into行之前满足条件的行,代码将抛出一个不会被处理的异常。

    原帖的第二个版本没有这个问题,一般首选。

    也就是说,使用异常会产生少量开销,如果您 100% 确定数据不会更改,则可以使用 count(*),但我不建议这样做。

    我在 32 位 Windows 上的 Oracle 10.2.0.1 上运行了这些基准测试。我只看过去的时间。还有其他测试工具可以提供更多详细信息(例如闩锁计数和使用的内存)。

    SQL>create table t (NEEDED_FIELD number, COND number);
    

    表已创建。

    SQL>insert into t (NEEDED_FIELD, cond) values (1, 0);
    

    已创建 1 行。

    declare
      otherVar  number;
      cnt number;
    begin
      for i in 1 .. 50000 loop
         select count(*) into cnt from t where cond = 1;
    
         if (cnt = 1) then
           select NEEDED_FIELD INTO otherVar from t where cond = 1;
         else
           otherVar := 0;
         end if;
       end loop;
    end;
    /
    

    PL/SQL 过程成功完成。

    经过:00:00:02.70

    declare
      otherVar  number;
    begin
      for i in 1 .. 50000 loop
         begin
           select NEEDED_FIELD INTO otherVar from t where cond = 1;
         exception
           when no_data_found then
             otherVar := 0;
         end;
       end loop;
    end;
    /
    

    PL/SQL 过程成功完成。

    经过:00:00:03.06

    【讨论】:

      【解决方案2】:

      由于 SELECT INTO 假定将返回单行,因此您可以使用以下形式的语句:

      SELECT MAX(column)
        INTO var
        FROM table
       WHERE conditions;
      
      IF var IS NOT NULL
      THEN ...
      

      如果可用,SELECT 将为您提供值,并为您提供 NULL 值而不是 NO_DATA_FOUND 异常。 MAX() 引入的开销将最小到零,因为结果集包含单行。与基于游标的解决方案相比,它还具有紧凑的优点,并且不会像原始帖子中的两步解决方案那样容易受到并发问题的影响。

      【讨论】:

      • 此解决方案的缺点是它会隐藏您可能不想隐藏的其他异常情况,因为它不应该发生,例如 TOO_MANY_ROWS 异常。
      【解决方案3】:

      @Steve 代码的替代方案。

      DECLARE
        CURSOR foo_cur IS 
          SELECT NEEDED_FIELD WHERE condition ;
      BEGIN
        FOR foo_rec IN foo_cur LOOP
           ...
        END LOOP;
      EXCEPTION
        WHEN OTHERS THEN
          RAISE;
      END ;
      

      如果没有数据,则不执行循环。光标 FOR 循环是要走的路——它们有助于避免大量的内务处理。更紧凑的解决方案:

      DECLARE
      BEGIN
        FOR foo_rec IN (SELECT NEEDED_FIELD WHERE condition) LOOP
           ...
        END LOOP;
      EXCEPTION
        WHEN OTHERS THEN
          RAISE;
      END ;
      

      如果您在编译时知道完整的 select 语句,这会起作用。

      【讨论】:

        【解决方案4】:

        @DCookie

        我只想指出,你可以省略那些说

        EXCEPTION  
          WHEN OTHERS THEN    
            RAISE;
        

        如果你把异常块全部去掉,你会得到同样的效果,为异常报告的行号将是实际抛出异常的行,而不是异常块中重新抛出异常的行-提高。

        【讨论】:

        • 当然。我只是留下了它,因为它可能很有用,具体取决于您在 FOR 循环中所做的事情以及您在异常处理程序中放入的内容。
        【解决方案5】:

        Stephen Darlington 提出了一个非常好的观点,如果我使用以下方法将表格填充到 10000 行,您可以看到,如果您更改我的基准以使用更实际大小的表格:

        begin 
          for i in 2 .. 10000 loop
            insert into t (NEEDED_FIELD, cond) values (i, 10);
          end loop;
        end;
        

        然后重新运行基准测试。 (我必须将循环次数减少到 5000 才能获得合理的时间)。

        declare
          otherVar  number;
          cnt number;
        begin
          for i in 1 .. 5000 loop
             select count(*) into cnt from t where cond = 0;
        
             if (cnt = 1) then
               select NEEDED_FIELD INTO otherVar from t where cond = 0;
             else
               otherVar := 0;
             end if;
           end loop;
        end;
        /
        
        PL/SQL procedure successfully completed.
        
        Elapsed: 00:00:04.34
        
        declare
          otherVar  number;
        begin
          for i in 1 .. 5000 loop
             begin
               select NEEDED_FIELD INTO otherVar from t where cond = 0;
             exception
               when no_data_found then
                 otherVar := 0;
             end;
           end loop;
        end;
        /
        
        PL/SQL procedure successfully completed.
        
        Elapsed: 00:00:02.10
        

        除此之外的方法现在的速度是原来的两倍多。因此,对于几乎所有情况,方法:

        SELECT NEEDED_FIELD INTO var WHERE condition;
        EXCEPTION
        WHEN NO_DATA_FOUND....
        

        是要走的路。它会给出正确的结果,并且通常是最快的。

        【讨论】:

          【解决方案6】:

          如果这很重要,您真的需要对这两个选项进行基准测试!

          话虽如此,我一直使用异常方法,原因是最好只访问数据库一次。

          【讨论】:

            【解决方案7】:

            是的,你没有使用光标

            DECLARE
              CURSOR foo_cur IS 
                SELECT NEEDED_FIELD WHERE condition ;
            BEGIN
              OPEN foo_cur;
              FETCH foo_cur INTO foo_rec;
              IF foo_cur%FOUND THEN
                 ...
              END IF;
              CLOSE foo_cur;
            EXCEPTION
              WHEN OTHERS THEN
                CLOSE foo_cur;
                RAISE;
            END ;
            

            诚然,这是更多的代码,但它不使用 EXCEPTIONs 作为流控制,我从 Steve Feuerstein 的 PL/SQL Programming 一书中学到了我的大部分 PL/SQL,我相信这是一件好事。

            我不知道这是否更快(我现在很少做 PL/SQL)。

            【讨论】:

            • 谢谢,史蒂夫。请参阅我上面的编辑。这有什么不同吗?
            • 哦,呵呵!当然会奏效。好的,需要更多的咖啡。谢谢。
            【解决方案8】:

            与嵌套游标循环相比,一种更有效的方法是使用一个游标循环和表之间的外部连接。

            BEGIN
                FOR rec IN (SELECT a.needed_field,b.other_field
                              FROM table1 a
                              LEFT OUTER JOIN table2 b
                                ON a.needed_field = b.condition_field
                             WHERE a.column = ???)
                LOOP
                   IF rec.other_field IS NOT NULL THEN
                     -- whatever processing needs to be done to other_field
                   END IF;
                END LOOP;
            END;
            

            【讨论】:

            • 这绝对是更好的方法,因为您可以避免单独的 SQL 语句。 Oracle 可以更好地优化外连接选择,因为它知道您对 table1 中的每一行所做的操作。
            • 在此示例中,最好将外部联接更改为内部联接并删除 IF 条件。
            【解决方案9】:

            使用 for 循环时不必使用 open。

            declare
            cursor cur_name is  select * from emp;
            begin
            for cur_rec in cur_name Loop
                dbms_output.put_line(cur_rec.ename);
            end loop;
            End ;
            

            declare
            cursor cur_name is  select * from emp;
            cur_rec emp%rowtype;
            begin
            Open cur_name;
            Loop
            Fetch cur_name into  Cur_rec;
               Exit when cur_name%notfound;
                dbms_output.put_line(cur_rec.ename);
            end loop;
            Close cur_name;
            End ;
            

            【讨论】:

              【解决方案10】:

              可能在这里打死马,但我将光标作为循环的基准,它的性能与 no_data_found 方法差不多:

              declare
                otherVar  number;
              begin
                for i in 1 .. 5000 loop
                   begin
                     for foo_rec in (select NEEDED_FIELD from t where cond = 0) loop
                       otherVar := foo_rec.NEEDED_FIELD;
                     end loop;
                     otherVar := 0;
                   end;
                 end loop;
              end;
              

              PL/SQL 过程成功完成。

              经过:00:00:02.18

              【讨论】:

                【解决方案11】:

                count(*) 永远不会引发异常,因为无论如何它总是返回实际计数或 0 - 零。我会使用计数。

                【讨论】:

                  【解决方案12】:

                  陈述的第一个(优秀)答案-

                  count() 方法不安全。如果另一个会话在 count(*) 行之后、select ... into 行之前删除了满足条件的行,代码将抛出一个不会被处理的异常。

                  并非如此。在给定的逻辑工作单元内,Oracle 是完全一致的。即使有人提交了删除计数和选择之间的行,对于活动会话,Oracle 也会从日志中获取数据。如果不能,您将收到“快照太旧”错误。

                  【讨论】:

                  • 只有在isolation_level 设置为serializable 时才这样。
                  猜你喜欢
                  • 1970-01-01
                  • 2021-11-29
                  • 2017-12-21
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-04-17
                  • 2017-04-13
                  • 1970-01-01
                  相关资源
                  最近更新 更多