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.