Oracle PL / SQL - Le eccezioni NO_DATA_FOUND sono dannose per le prestazioni delle procedure memorizzate?
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.
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.