28

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

4

12 回答 12

34

I would not use an explicit cursor to do this. Steve F. no longer advises people to use explicit cursors when an implicit cursor could be used.

The method with count(*) is unsafe. If another session deletes the row that met the condition after the line with the count(*), and before the line with the select ... into, the code will throw an exception that will not get handled.

The second version from the original post does not have this problem, and it is generally preferred.

That said, there is a minor overhead using the exception, and if you are 100% sure the data will not change, you can use the count(*), but I recommend against it.

I ran these benchmarks on Oracle 10.2.0.1 on 32 bit Windows. I am only looking at elapsed time. There are other test harnesses that can give more details (such as latch counts and memory used).

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

Table created.

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

1 row created.

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 procedure successfully completed.

Elapsed: 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 procedure successfully completed.

Elapsed: 00:00:03.06

于 2008-10-21T15:46:55.837 回答
7

Since SELECT INTO assumes that a single row will be returned, you can use a statement of the form:

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

IF var IS NOT NULL
THEN ...

The SELECT will give you the value if one is available, and a value of NULL instead of a NO_DATA_FOUND exception. The overhead introduced by MAX() will be minimal-to-zero since the result set contains a single row. It also has the advantage of being compact relative to a cursor-based solution, and not being vulnerable to concurrency issues like the two-step solution in the original post.

于 2008-10-21T19:19:17.420 回答
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 ;

The loop is not executed if there is no data. Cursor FOR loops are the way to go - they help avoid a lot of housekeeping. An even more compact solution:

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

Which works if you know the complete select statement at compile time.

于 2008-10-21T15:42:44.143 回答
4

@DCookie

I just want to point out that you can leave off the lines that say

EXCEPTION  
  WHEN OTHERS THEN    
    RAISE;

You'll get the same effect if you leave off the exception block all together, and the line number reported for the exception will be the line where the exception is actually thrown, not the line in the exception block where it was re-raised.

于 2008-10-21T15:53:27.010 回答
3

Stephen Darlington makes a very good point, and you can see that if you change my benchmark to use a more realistically sized table if I fill the table out to 10000 rows using the following:

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

Then re-run the benchmarks. (I had to reduce the loop counts to 5000 to get reasonable times).

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

The method with the exception is now more than twice as fast. So, for almost all cases,the method:

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

is the way to go. It will give correct results and is generally the fastest.

于 2008-10-21T16:12:12.900 回答
2

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

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

于 2008-10-21T13:57:03.640 回答
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 ;

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

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

于 2008-10-21T13:54:41.227 回答
1

Rather than having nested cursor loops a more efficient approach would be to use one cursor loop with an outer join between the tables.

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;
于 2008-10-23T10:19:16.213 回答
0

May be beating a dead horse here, but I bench-marked the cursor for loop, and that performed about as well as the no_data_found method:

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 procedure successfully completed.

Elapsed: 00:00:02.18

于 2008-10-21T16:18:32.857 回答
0

you dont have to use open when you are using for loops.

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 ;

or

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 ;
于 2008-12-18T07:11:47.863 回答
0

The count(*) will never raise exception because it always returns actual count or 0 - zero, no matter what. I'd use count.

于 2013-01-03T13:26:42.173 回答
0

The first (excellent) answer stated -

The method with count() is unsafe. If another session deletes the row that met the condition after the line with the count(*), and before the line with the select ... into, the code will throw an exception that will not get handled.

Not so. Within a given logical Unit of Work Oracle is totally consistent. Even if someone commits the delete of the row between a count and a select Oracle will, for the active session, obtain the data from the logs. If it cannot, you will get a "snapshot too old" error.

于 2013-12-18T03:04:40.640 回答