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

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

  •  03-07-2019
  •  | 
  •  

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.

Était-ce utile?

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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top