Oracle PL / SQL - Le eccezioni NO_DATA_FOUND sono dannose per le prestazioni delle procedure memorizzate?

StackOverflow https://stackoverflow.com/questions/221909

  •  03-07-2019
  •  | 
  •  

Domanda

Sto scrivendo una procedura memorizzata che deve contenere molti condizionamenti. Con la conoscenza generale della codifica C # .NET che le eccezioni possono danneggiare le prestazioni, ho sempre evitato di usarle anche in PL / SQL. Il mio condizionamento in questo processo memorizzato ruota principalmente attorno all'esistenza o meno di un record, che potrei fare in due modi:

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

-o -

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

Il secondo caso mi sembra un po 'più elegante, perché quindi posso usare NEEDED_FIELD, che avrei dovuto selezionare nella prima istruzione dopo la condizione nel primo caso. Meno codice. Ma se la procedura memorizzata verrà eseguita più velocemente utilizzando COUNT (*), non mi dispiace digitare un po 'di più per compensare la velocità di elaborazione.

Qualche suggerimento? Mi sto perdendo un'altra possibilità?

Modifica Avrei dovuto menzionare che questo è già nidificato in un FOR LOOP. Non sono sicuro che questo faccia la differenza con l'uso di un cursore, poiché non credo di poter DICHIARARE il cursore come una selezione in FOR LOOP.

È stato utile?

Soluzione

Non vorrei usare un cursore esplicito per fare questo. Steve F. non consiglia più alle persone di utilizzare cursori espliciti quando è possibile utilizzare un cursore implicito.

Il metodo con count (*) non è sicuro. Se un'altra sessione elimina la riga che ha soddisfatto la condizione dopo la riga con il count (*) , e prima della riga con il seleziona ... in , il codice verrà lanciato un'eccezione che non verrà gestita.

La seconda versione del post originale non presenta questo problema ed è generalmente preferita.

Detto questo, c'è un overhead minore usando l'eccezione e se sei sicuro al 100% che i dati non cambieranno, puoi usare il count (*) , ma sconsiglio.

Ho eseguito questi benchmark su Oracle 10.2.0.1 su Windows a 32 bit . Sto solo guardando il tempo trascorso. Esistono altri cablaggi di prova che possono fornire maggiori dettagli (come il conteggio dei latch e la memoria utilizzata).

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

Tabella creata.

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

1 riga creata.

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;
/
  

Procedura PL / SQL completata correttamente.

     

Scaduto: 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;
/
  

Procedura PL / SQL completata correttamente.

     

Scaduto: 00:00:03.06

Altri suggerimenti

Poiché SELECT INTO presuppone che verrà restituita una singola riga, è possibile utilizzare un'istruzione del modulo:

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

IF var IS NOT NULL
THEN ...

SELEZIONA ti darà il valore se disponibile e un valore NULL anziché un'eccezione NO_DATA_FOUND. Il sovraccarico introdotto da MAX () sarà da minimo a zero poiché il set di risultati contiene una singola riga. Ha anche il vantaggio di essere compatto rispetto a una soluzione basata su cursore e di non essere vulnerabile a problemi di concorrenza come la soluzione in due passaggi nel post originale.

Un'alternativa al codice di @ 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 ;

Il ciclo non viene eseguito se non ci sono dati. I loop Cursor FOR sono la strada da percorrere: aiutano a evitare un sacco di pulizie. Una soluzione ancora più compatta:

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

Che funziona se si conosce l'istruzione select completa al momento della compilazione.

@DCookie

Voglio solo sottolineare che puoi lasciare le righe che dicono

EXCEPTION  
  WHEN OTHERS THEN    
    RAISE;

Otterrai lo stesso effetto se lasci il blocco delle eccezioni tutti insieme e il numero di riga riportato per l'eccezione sarà la riga in cui viene effettivamente generata l'eccezione, non la riga nel blocco di eccezioni in cui si trovava -raised.

Stephen Darlington fa un ottimo punto e puoi vedere che se cambi il mio benchmark per usare una tabella di dimensioni più realistiche se riempio la tabella fino a 10000 righe usando quanto segue:

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

Quindi rieseguire i benchmark. (Ho dovuto ridurre il numero di loop a 5000 per ottenere tempi ragionevoli).

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

Il metodo con l'eccezione è ora più del doppio della velocità. Quindi, per quasi tutti i casi, il metodo:

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

è la strada da percorrere. Fornirà risultati corretti ed è generalmente il più veloce.

Se è importante devi davvero confrontare entrambe le opzioni!

Detto questo, ho sempre usato il metodo dell'eccezione, il ragionamento è che è meglio colpire il database solo una volta.

Sì, ti stai perdendo usando i cursori

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 ;

è vero che questo è più codice, ma non usa EXCEPTIONs come controllo di flusso che, avendo appreso la maggior parte del mio PL / SQL dal libro di programmazione PL / SQL di Steve Feuerstein, credo sia una buona cosa.

Se questo è più veloce o no non lo so (oggi faccio pochissimo PL / SQL).

Invece di avere loop di cursore nidificati, un approccio più efficiente sarebbe quello di utilizzare un loop di cursore con un join esterno tra le tabelle.

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;

non devi usare open quando usi i loop.

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 ;

o

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 ;

Qui si può battere un cavallo morto, ma ho contrassegnato in panchina il cursore per loop, e quello ha funzionato così come il metodo 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;

Procedura PL / SQL completata correttamente.

Scaduto: 00: 00: 02.18

Il conteggio (*) non genererà mai eccezioni perché restituisce sempre il conteggio effettivo o 0 - zero, non importa quale. Userei count.

La prima (eccellente) risposta dichiarata -

Il metodo con count () non è sicuro. Se un'altra sessione elimina la riga che ha soddisfatto la condizione dopo la riga con il conteggio (*) e prima della riga con l'opzione select ... in, il codice genererà un'eccezione che non verrà gestita.

Non così. All'interno di una determinata unità di lavoro logica Oracle è totalmente coerente. Anche se qualcuno commette l'eliminazione della riga tra un conteggio e un Oracle selezionato, per la sessione attiva otterrà i dati dai registri. In caso contrario, otterrai un'istantanea "troppo vecchia" di errore.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top