Ecritures cohérentes – observation d’un redémarrage, par Tom Kyte (2ème partie)

Cet article est la traduction d’un article de Tom Kyte sur son blog (L’article original en anglais se trouve ici).


Il est conseillé lire préalablement la première partie:

Voir un redémarrage d’une requête update est plus plus facile qu’on ne pense.
En fait, nous allons même en voir un avec une simple table d’une seule ligne.

Voici la table que nous allons utiliser pour notre test:

ops$tkyte@ORA10G> create table t ( x int, y int );
Table created.
 
ops$tkyte@ORA10G> insert into t values ( 1, 1 );
1 row created.
 
ops$tkyte@ORA10G> commit;
Commit complete.

Pour observer le restart, nous avons juste besoin d’un trigger qui va afficher des informations. Nous allons utiliser un trigger BEFORE UPDATE FOR EACH ROW qui va simplement afficher l’ancienne et la nouvelle valeur de la ligne lors d’un update.

ops$tkyte@ORA10G> create or replace trigger t_bufer
  2  before update on t for each row
  3  begin
  4          dbms_output.put_line
  5          ( 'old.x = ' || :old.x ||
  6            ', old.y = ' || :old.y );
  7          dbms_output.put_line
  8          ( 'new.x = ' || :new.x ||
  9            ', new.y = ' || :new.y );
10  end;
11  /
Trigger created.

Et maintenant nous faisons un update sur la ligne:

ops$tkyte@ORA10G> set serveroutput on
ops$tkyte@ORA10G> update t set x = x+1;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
1 row updated.

jusqu’ici, tout ce passe comme on s’y attend: le trigger s’est déclenché une fois, et nous voyons les anciennes et nouvelles valeurs. Remarquez que nous n’avons pas encore fait de commit, la ligne est toujours vérouillée. Dans une autre session, nous allons faire l’update suivant:

ops$tkyte@ORA10G> set serveroutput on
ops$tkyte@ORA10G> update t set x = x+1 where x > 0;

La requête se bloque tout de suite, évidemment, puisque la première session a verrouillé la ligne. Si nous retournons dans la première session, pour faire un commit, voici ce qu’affiche la deuxième session (j’ai remis l’update pour que ça soit plus clair)

ops$tkyte@ORA10G> update t set x = x+1 where x > 0;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
old.x = 2, old.y = 1
new.x = 3, new.y = 1
1 row updated.

Comme vous pouvez le voir, le trigger a vu deux version de la ligne ici. Le trigger sur la ligne s’est déclenché deux fois:

  • Une première fois avec la version d’origine de la ligne, et les nouvelles valeurs calculées par l’update
  • et une deuxième fois avec la version finale de la ligne telle qu’elle a réellement été modifiée.

Du fait que c’est un trigger BEFORE FOR EACH ROW, Oracle a vu la version cohérente (read-consistent) de notre ligne et la mise à jour que nous souhaitons y faire. Mais c’est après le déclenchement du trigger BEFORE FOR EACH ROW qu’ Oracle a récupéré le block en mode courant (current mode) pour y faire la mise à jour. Oracle doit attendre la fin du déclenchement du trigger parce que le trigger peut modifier les valeurs :new. Donc, Oracle ne peut modifier le bloc qu’une fois que le trigger ait fini de s’exécuter, et cela peut prendre du temps. Vu qu’un bloc ne peut être pris en mode courant que par une seule session à la fois, Oracle doit limiter le temps passé dans ce mode.

Donc après que le trigger se soit exécuté, Oracle prends le bloc en mode courant et s’aperçoit que la colonne X, qui a été utilisée pour trouver la ligne, n’a plus la même valeur. Et comme X, qui a été utilisé pour trouver cette ligne, a été modifiée, alors la base de données décide de redémarrer notre requête.

Notez que le fait que la valeur de X soit passée de 1 à 2 ne fait pas sortir la ligne du périmètre de la requête. Elle va quand même être touchée par notre update. C’est plutôt le fait que X ait été utilisé pour trouver la ligne, et que la valeur en lecture cohérente (ici 1) soit différente de la valeur en lecture courante (ici 2). Et ensuite, lors du restart, le trigger voit la valeur X=2 (qui vient de la modification faite par l’autre session) comme valeur de :old et la valeur X=3 pour :new

Donc cela prouve qu’un update restart a eu lieu. Il a fallu un trigger pour le voir, sinon ils sont en généralement indétectables. Cela ne veut pas dire qu’on ne peut pas voir d’autres symptômes, comme un gros update qui doit faire un rollback de toutes les modifications effectuées lorsqu’il a découvert qu’une ligne provoqué le restart. Cela veut juste dire qu’il est très difficile de pouvoir affirmer que ce type de symptôme vient du restart.

Il y a une autre chose intéressante, c’est que les triggers eux-mêmes peuvent être la cause d’un restart, même si la requête en elle-même ne le demande pas. Normalement, c’est les colonnes qui sont utilisées dans la clause WHERE d’un UPDATE ou DELETE qui sont utilisées pour déterminer si les modifications nécessitent un redémarrage de la requête. Oracle va faire une lecture en consistent read et lorsqu’il prends le bloc en current mode, il va redémarrer la requête si l’une de ces colonnes a été modifiée. Normalement, les autres colonnes ne sont pas comparées. Par exemple, si nous ré-exécutons la l’exemple précédant en mettant WHERE Y>0 pour trouver la ligne au lieu de X>0 :

ops$tkyte@ORA10G> update t set x = x+1 where y > 0;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
old.x = 2, old.y = 1
new.x = 3, new.y = 1
1 row updated.

On peut se demander pourquoi Oracle encore déclenché le trigger deux fois alors qu’il regardait la valeur de Y. Et on pourrait même croire que toutes les colonnes de la ligne sont examinées. Comme on peut le voir, ici on a eu un update restart alors qu’on filtrait les lignes avec Y>0 et qu’on a jamais modifié Y.

Mais si on recrée le trigger en affichant juste un message ‘fired‘ pour dire qu’il a été déclanché, sans faire référence à :new et :old

ops$tkyte@ORA10G> create or replace trigger t_bufer
  2  before update on t for each row
  3  begin
  4          dbms_output.put_line( 'fired' );
  5  end;
  6  /
Trigger created.
 
ops$tkyte@ORA10G> update t set x = x+1;
fired
1 row updated.

et que l’on va à nouveau dans la deuxième session pour exécuter l’update, on voit qu’il bloque, bien sûr. Et après avoir fait le commit sur la première session, la deuxième session se débloque et voici ce qu’elle affiche:

ops$tkyte@ORA10G> update t set x = x+1 where y > 0;
fired
1 row updated.

Le trigger ne s’est déclenché qu’une seule fois, pas deux.

Cela montre que les valeurs des colonnes référencées par :new et :old dans le trigger sont aussi utilisées par Oracle pour vérifier s’il faut faire un restart.
Lorsque le trigger référencait :new.X et :old.X alors les valeurs cohérentes et courantes de X ont été comparées. Et cela a entraîner un restart. Mais lorsqu’on a enlevé ces références, le restart n’a plus eu lieu.

La règle est que c’est l’ensemble des colonnes utilisées dans la clause WHERE, plus les colonnes référencées dans les triggers ligne (row trigger) qui sont comparées. La version cohérente et la version courante de la ligne vont être comparées, et si une de ces colonnes a changé, alors la requête va redémarrer.

A noter que cela peut aider à comprendre pourquoi un trigger AFTER FOR EACH ROW est plus performant qu’un trigger BEFORE FOR EACH ROW. Le trigger AFTER n’a pas les memes conséquences. Ce qui nous amène à la question ‘En quoi est-ce que ce comportement interne nous concerne ?’.

A suivre…

La troisième partie sera traduite prochainement:

Laisser un commentaire