Oracle PL / SQL - NO_DATA_FOUND异常对存储过程性能有害吗?

时间:2008-10-21 13:37:56

标签: sql oracle exception plsql

我正在编写一个需要在其中进行大量调整的存储过程。根据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中的选择进行DECLARE。

12 个答案:

答案 0 :(得分:30)

我不会使用显式游标来执行此操作。当可以使用隐式游标时,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);
  

创建了一行。

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

答案 1 :(得分:7)

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

SELECT MAX(column)
  INTO var
  FROM table
 WHERE conditions;

IF var IS NOT NULL
THEN ...

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

答案 2 :(得分:6)

@ 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语句,那么这是有效的。

答案 3 :(得分:4)

@DCookie

我只是想指出,你可以放弃说

的界限
EXCEPTION  
  WHEN OTHERS THEN    
    RAISE;

如果你将异常块全部放在一起,你将获得相同的效果,并且为异常报告的行号将是实际抛出异常的行,而不是异常块中的行所在的行-raised。

答案 4 :(得分:3)

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....

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

答案 5 :(得分:2)

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

话虽如此,我总是使用异常方法,因为最好只打一次数据库。

答案 6 :(得分:1)

是的,你错过了使用游标

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 ;

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

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

答案 7 :(得分:1)

与嵌套游标循环不同,一种更有效的方法是使用一个游标循环与表之间的外部联接。

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;

答案 8 :(得分:0)

可能会在这里击败一匹死马,但我将光标标记为循环,并且与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

答案 9 :(得分:0)

当你使用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 :(得分:0)

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

答案 11 :(得分:0)

第一个(优秀的)答案说明了 -

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

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