web-dev-qa-db-fra.com

Oracle PL/SQL - Les exceptions NO_DATA_FOUND sont-elles mauvaises pour les performances des procédures stockées?

J'écris une procédure stockée qui nécessite beaucoup de conditionnement. 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 cette procédure stockée 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;
....

-ou-

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 j’aurais dû 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 fonctionne plus rapidement avec COUNT (*), cela ne me dérange pas de taper un peu plus pour compenser la vitesse de traitement.

Des allusions? Est-ce que je manque une autre possibilité?

MODIFIER J'aurais dû mentionner que tout cela est déjà imbriqué dans un FOR LOOP. Je ne sais pas si cela fait 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.

23
AJ.

Je ne voudrais pas utiliser un curseur explicite pour le faire. Steve F. déconseille aux utilisateurs d'utiliser des curseurs explicites lorsqu'un curseur implicite peut être utilisé. 

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 count(*) et avant la ligne avec select ... into, le code lève une exception qui ne sera pas traitée.

La deuxième version de l'article d'origine n'a pas ce problème, et il est généralement préféré.

Cela dit, il existe une surcharge mineure avec l'exception, et si vous êtes sûr à 100% que les données ne changeront pas, vous pouvez utiliser la fonction count(*), mais je le déconseille.

J'ai exécuté ces tests de performance sur Oracle 10.2.0.1 sur 32 bits Windows. 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 rangée 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.

Écoulé: 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.

Écoulé: 00: 00: 03.06

30
RussellH

SELECT INTO supposant 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 et la 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 concurrence tels que la solution en deux étapes de la publication originale.

7
Noah Yetter

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 de tâches ménagères. 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.

6
DCookie

@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 tout le bloc d'exception 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é relevée.

4
RussellH

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;

Puis relancez les repères. (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 à l'exception est maintenant plus de 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 le chemin à parcourir. Il donnera des résultats corrects et est généralement le plus rapide. 

3
RussellH

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 était préférable de ne toucher la base de données qu’une fois.

2
Stephen Darlington

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 davantage de code, mais il n’utilise pas EXCEPTION comme contrôle de flux et, 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 pas, je ne sais pas (je fais très peu de PL/SQL de nos jours).

1
Steve Bosman

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;
1
pablo

La première (excellente) réponse a déclaré -

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 compte (*) et avant la ligne avec le choix ... dans, 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 ce n'est pas le cas, vous obtiendrez une erreur "instantané trop ancien".

0

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

Peut-être bat-on un cheval mort ici, mais j'ai jalonné le curseur pour la boucle, et cela a fonctionné 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.

Écoulé: 00: 00: 02.18

0
RussellH

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 arrive. J'utiliserais le compte.

0
Art