Oracle
 sql >> Base de données >  >> RDS >> Oracle

Oracle :performances de la collecte en bloc

Au sein d'Oracle, il existe une machine virtuelle (VM) SQL et une VM PL/SQL. Lorsque vous devez passer d'une VM à l'autre, vous encourez le coût d'un changement de contexte. Individuellement, ces changements de contexte sont relativement rapides, mais lorsque vous effectuez un traitement ligne par ligne, ils peuvent s'additionner pour représenter une fraction importante du temps passé par votre code. Lorsque vous utilisez des liaisons en bloc, vous déplacez plusieurs lignes de données d'une machine virtuelle à l'autre avec un seul changement de contexte, ce qui réduit considérablement le nombre de changements de contexte et accélère votre code.

Prenons, par exemple, un curseur explicite. Si j'écris quelque chose comme ça

DECLARE
  CURSOR c 
      IS SELECT *
           FROM source_table;
  l_rec source_table%rowtype;
BEGIN
  OPEN c;
  LOOP
    FETCH c INTO l_rec;
    EXIT WHEN c%notfound;

    INSERT INTO dest_table( col1, col2, ... , colN )
      VALUES( l_rec.col1, l_rec.col2, ... , l_rec.colN );
  END LOOP;
END;

puis chaque fois que j'exécute la récupération, je suis

  • Exécution d'un changement de contexte de la VM PL/SQL vers la VM SQL
  • Demander à la machine virtuelle SQL d'exécuter le curseur pour générer la ligne de données suivante
  • Effectuer un autre changement de contexte de la VM SQL vers la VM PL/SQL pour renvoyer ma seule ligne de données

Et chaque fois que j'insère une ligne, je fais la même chose. J'engage le coût d'un changement de contexte pour expédier une ligne de données de la machine virtuelle PL/SQL à la machine virtuelle SQL, en demandant au SQL d'exécuter le INSERT instruction, puis encourir le coût d'un autre changement de contexte vers PL/SQL.

Si source_table a 1 million de lignes, c'est 4 millions de changements de contexte qui représenteront probablement une fraction raisonnable du temps écoulé de mon code. Si par contre je fais un BULK COLLECT avec un LIMIT de 100, je peux éliminer 99 % de mes changements de contexte en récupérant 100 lignes de données de la machine virtuelle SQL dans une collection en PL/SQL chaque fois que j'engage le coût d'un changement de contexte et en insérant 100 lignes dans la table de destination à chaque fois que je subir un changement de contexte.

Si je peux réécrire mon code pour utiliser les opérations en masse

DECLARE
  CURSOR c 
      IS SELECT *
           FROM source_table;
  TYPE  nt_type IS TABLE OF source_table%rowtype;
  l_arr nt_type;
BEGIN
  OPEN c;
  LOOP
    FETCH c BULK COLLECT INTO l_arr LIMIT 100;
    EXIT WHEN l_arr.count = 0;

    FORALL i IN 1 .. l_arr.count
      INSERT INTO dest_table( col1, col2, ... , colN )
        VALUES( l_arr(i).col1, l_arr(i).col2, ... , l_arr(i).colN );
  END LOOP;
END;

Maintenant, chaque fois que j'exécute la récupération, je récupère 100 lignes de données dans ma collection avec un seul ensemble de changements de contexte. Et à chaque fois je fais mon FORALL insert, j'insère 100 lignes avec un seul ensemble de changements de contexte. Si source_table a 1 million de lignes, cela signifie que je suis passé de 4 millions de changements de contexte à 40 000 changements de contexte. Si les changements de contexte représentaient, disons, 20 % du temps écoulé de mon code, j'ai éliminé 19,8 % du temps écoulé.

Vous pouvez augmenter la taille de la LIMIT pour réduire davantage le nombre de changements de contexte, mais vous vous heurtez rapidement à la loi des rendements décroissants. Si vous avez utilisé un LIMIT de 1000 au lieu de 100, vous élimineriez 99,9 % des changements de contexte au lieu de 99 %. Cela signifierait cependant que votre collection utilisait 10 fois plus de mémoire PGA. Et cela n'éliminerait que 0,18 % de temps écoulé supplémentaire dans notre exemple hypothétique. Vous atteignez très rapidement un point où la mémoire supplémentaire que vous utilisez ajoute plus de temps que vous n'en gagnez en éliminant les changements de contexte supplémentaires. En général, un LIMIT quelque part entre 100 et 1000 est susceptible d'être le sweet spot.

Bien sûr, dans cet exemple, il serait encore plus efficace d'éliminer tous les changements de contexte et de tout faire dans une seule instruction SQL

INSERT INTO dest_table( col1, col2, ... , colN )
  SELECT col1, col2, ... , colN
    FROM source_table;

Cela n'aurait de sens que de recourir à PL/SQL en premier lieu si vous faites une sorte de manipulation des données de la table source que vous ne pouvez pas raisonnablement implémenter en SQL.

De plus, j'ai intentionnellement utilisé un curseur explicite dans mon exemple. Si vous utilisez des curseurs implicites, dans les versions récentes d'Oracle, vous bénéficiez des avantages d'un BULK COLLECT avec un LIMIT de 100 implicitement. Il existe une autre question StackOverflow qui traite des avantages relatifs des performances des curseurs implicites et explicites avec des opérations en bloc qui détaillent plus en détail ces rides particulières.