Oracle PL / SQL - Les exceptions NO_DATA_FOUND sont-elles mauvaises pour les performances des procédures stockées?
Question
J'écris une procédure stockée qui doit être conditionnée. Grâce à la connaissance générale du codage C # .NET selon laquelle les exceptions peuvent nuire aux performances, j'ai toujours évité de les utiliser également en PL / SQL. Mon conditionnement dans ce processus stocké dépend principalement de l'existence ou non d'un enregistrement, ce que je pourrais faire de deux manières:
SELECT COUNT(*) INTO var WHERE condition;
IF var > 0 THEN
SELECT NEEDED_FIELD INTO otherVar WHERE condition;
....
-or -
SELECT NEEDED_FIELD INTO var WHERE condition;
EXCEPTION
WHEN NO_DATA_FOUND
....
Le second cas me semble un peu plus élégant, car je peux alors utiliser NEEDED_FIELD, que je devrais sélectionner dans la première instruction après la condition dans le premier cas. Moins de code. Mais si la procédure stockée fonctionnera plus rapidement avec COUNT (*), cela ne me dérange pas de taper un peu plus pour rattraper la vitesse de traitement.
Des indices? Est-ce que je manque une autre possibilité?
EDIT J'aurais dû mentionner que tout cela est déjà imbriqué dans un FOR LOOP. Je ne suis pas sûr que cela fasse une différence avec l’utilisation d’un curseur, car je ne pense pas pouvoir déclarer le curseur comme une sélection dans la boucle FOR.
La solution
Je n’utiliserais pas de curseur explicite pour cela. Steve F. ne conseille plus aux personnes d'utiliser des curseurs explicites lorsqu'un curseur implicite pourrait être utilisé.
La méthode avec count (*)
est dangereuse. Si une autre session supprime la ligne qui remplit la condition après la ligne avec le compte (*)
et avant la ligne avec le select ... into
, le code sera renvoyé. une exception qui ne sera pas traitée.
La deuxième version du message d'origine n'a pas ce problème, et il est généralement préféré.
Cela dit, l’exception comporte une surcharge mineure, et si vous êtes sûr à 100% que les données ne changeront pas, vous pouvez utiliser le count (*)
, mais je le déconseille.
J'ai exécuté ces tests de performances sur Oracle 10.2.0.1 sur Windows 32 bits . Je ne regarde que le temps écoulé. Il existe d'autres faisceaux de test pouvant donner plus de détails (tels que le nombre de verrous et la mémoire utilisée).
SQL>create table t (NEEDED_FIELD number, COND number);
Table créée.
SQL>insert into t (NEEDED_FIELD, cond) values (1, 0);
1 ligne créée.
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;
/
Procédure PL / SQL terminée avec succès.
Elaped: 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;
/
Procédure PL / SQL terminée avec succès.
Elaped: 00: 00: 03.06
Autres conseils
Puisque SELECT INTO suppose qu'une seule ligne sera renvoyée, vous pouvez utiliser une instruction de la forme suivante:
SELECT MAX(column)
INTO var
FROM table
WHERE conditions;
IF var IS NOT NULL
THEN ...
Le SELECT vous donnera la valeur s'il en existe une, ainsi qu'une valeur NULL au lieu d'une exception NO_DATA_FOUND. La surcharge introduite par MAX () sera minimale à zéro car le jeu de résultats contient une seule ligne. Elle présente également l’avantage d’être compacte par rapport à une solution basée sur un curseur et de ne pas être vulnérable aux problèmes de simultanéité tels que la solution en deux étapes de la publication originale.
Une alternative au code de @ 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 ;
La boucle n'est pas exécutée s'il n'y a pas de données. Les boucles Cursor FOR sont la solution: elles permettent d’éviter beaucoup d’entretien ménager. Une solution encore plus compacte:
DECLARE
BEGIN
FOR foo_rec IN (SELECT NEEDED_FIELD WHERE condition) LOOP
...
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END ;
Ce qui fonctionne si vous connaissez l'instruction select complète au moment de la compilation.
@DCookie
Je veux juste souligner que vous pouvez laisser de côté les lignes qui disent
EXCEPTION
WHEN OTHERS THEN
RAISE;
Vous obtiendrez le même effet si vous laissez le bloc d'exceptions tous ensemble, et le numéro de ligne indiqué pour l'exception sera la ligne où l'exception est réellement levée, et non la ligne du bloc d'exceptions où elle a été renvoyée. élevé.
Stephen Darlington fait valoir un très bon point, et vous pouvez voir que si vous modifiez mon critère de référence pour utiliser un tableau de taille plus réaliste si je remplis le tableau sur 10 000 lignes en utilisant les éléments suivants:
begin
for i in 2 .. 10000 loop
insert into t (NEEDED_FIELD, cond) values (i, 10);
end loop;
end;
Ensuite, réexécutez les tests de performance. (J'ai dû réduire le nombre de boucles à 5000 pour obtenir des temps raisonnables).
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
La méthode avec l'exception est maintenant deux fois plus rapide. Donc, dans presque tous les cas, la méthode:
SELECT NEEDED_FIELD INTO var WHERE condition;
EXCEPTION
WHEN NO_DATA_FOUND....
est la voie à suivre. Il donnera des résultats corrects et est généralement le plus rapide.
Si c'est important, vous devez vraiment comparer les deux options!
Cela dit, j'ai toujours utilisé la méthode des exceptions, le raisonnement étant qu'il est préférable de ne frapper la base de données qu'une seule fois.
Oui, il vous manque des curseurs
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 ;
Certes, il s’agit de plus de code, mais il n’utilise pas EXCEPTION comme contrôle de flux. Après avoir appris la plupart de mes PL / SQL grâce au livre de programmation PL / SQL de Steve Feuerstein, j’estime être une bonne chose.
Que ce soit plus rapide ou non, je ne le sais pas (je ne fais que très peu de PL / SQL de nos jours).
Plutôt que d'avoir des boucles de curseur imbriquées, une approche plus efficace consisterait à utiliser une boucle de curseur avec une jointure externe entre les 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;
vous ne devez pas utiliser open lorsque vous utilisez des boucles for.
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 ;
ou
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 ;
Peut-être bat-il un cheval mort ici, mais j'ai jalonné le curseur pour la boucle, et cela a fonctionné à peu près aussi bien que la méthode 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;
Procédure PL / SQL terminée avec succès.
Temps écoulé: 00: 00: 02.18
Le décompte (*) ne déclenchera jamais d’exception car il renvoie toujours le décompte réel ou 0 - zéro, quoi qu’il en soit. J'utiliserais le compte.
La première (excellente) réponse énoncée -
La méthode avec count () n'est pas sûre. Si une autre session supprime la ligne qui remplit la condition après la ligne avec le nombre (*) et avant la ligne avec le paramètre select ... into, le code lève une exception qui ne sera pas traitée.
Pas si. Au sein d'une unité de travail logique, Oracle est totalement cohérent. Même si une personne valide la suppression de la ligne entre un compte et une sélection Oracle, pour la session active, obtient les données des journaux. Si tel n'est pas le cas, vous obtiendrez un "instantané trop ancien". erreur.