Dentro do Oracle, há uma máquina virtual SQL (VM) e uma VM PL/SQL. Quando você precisa mover de uma VM para outra VM, você incorre no custo de uma mudança de contexto. Individualmente, essas mudanças de contexto são relativamente rápidas, mas quando você está processando linha por linha, elas podem representar uma fração significativa do tempo que seu código está gastando. Ao usar associações em massa, você move várias linhas de dados de uma VM para outra com uma única mudança de contexto, reduzindo significativamente o número de mudanças de contexto, tornando seu código mais rápido.
Tome, por exemplo, um cursor explícito. Se eu escrever algo assim
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;
então toda vez que eu executo a busca, eu sou
- Executando uma mudança de contexto da PL/SQL VM para a SQL VM
- Solicitando que a VM SQL execute o cursor para gerar a próxima linha de dados
- Realizar outra mudança de contexto da VM SQL de volta para a VM PL/SQL para retornar minha única linha de dados
E toda vez que insiro uma linha, estou fazendo a mesma coisa. Estou incorrendo no custo de uma mudança de contexto para enviar uma linha de dados da PL/SQL VM para a SQL VM, solicitando que o SQL execute o
INSERT
instrução e, em seguida, incorrer no custo de outra mudança de contexto de volta para PL/SQL. Se
source_table
tem 1 milhão de linhas, são 4 milhões de mudanças de contexto que provavelmente serão responsáveis por uma fração razoável do tempo decorrido do meu código. Se, por outro lado, eu fizer um BULK COLLECT
com um LIMIT
de 100, posso eliminar 99% das minhas mudanças de contexto recuperando 100 linhas de dados da VM SQL em uma coleção em PL/SQL toda vez que incorrer no custo de uma mudança de contexto e inserir 100 linhas na tabela de destino toda vez que incorrer em uma mudança de contexto lá. Se puder reescrever meu código para fazer uso de operações em massa
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;
Agora, toda vez que executo a busca, recupero 100 linhas de dados em minha coleção com um único conjunto de mudanças de contexto. E toda vez que eu faço meu
FORALL
insert, estou inserindo 100 linhas com um único conjunto de mudanças de contexto. Se source_table
tem 1 milhão de linhas, isso significa que eu passei de 4 milhões de mudanças de contexto para 40.000 mudanças de contexto. Se as mudanças de contexto foram responsáveis por, digamos, 20% do tempo decorrido do meu código, eliminei 19,8% do tempo decorrido. Você pode aumentar o tamanho do
LIMIT
para reduzir ainda mais o número de mudanças de contexto, mas você rapidamente atinge a lei dos retornos decrescentes. Se você usou um LIMIT
de 1000 em vez de 100, você eliminaria 99,9% das mudanças de contexto em vez de 99%. Isso significaria que sua coleção estava usando 10x mais memória PGA, no entanto. E isso eliminaria apenas 0,18% a mais de tempo decorrido em nosso exemplo hipotético. Você chega rapidamente a um ponto em que a memória adicional que está usando adiciona mais tempo do que economiza, eliminando mudanças de contexto adicionais. Em geral, um LIMIT
em algum lugar entre 100 e 1000 é provável que seja o ponto ideal. Claro, neste exemplo, seria ainda mais eficiente eliminar todas as mudanças de contexto e fazer tudo em uma única instrução SQL
INSERT INTO dest_table( col1, col2, ... , colN )
SELECT col1, col2, ... , colN
FROM source_table;
Só faria sentido recorrer a PL/SQL em primeiro lugar se você estiver fazendo algum tipo de manipulação dos dados da tabela de origem que você não pode implementar razoavelmente no SQL.
Além disso, usei um cursor explícito no meu exemplo intencionalmente. Se você estiver usando cursores implícitos, em versões recentes do Oracle, você obtém os benefícios de um
BULK COLLECT
com um LIMIT
de 100 implicitamente. Há outra pergunta do StackOverflow que discute os benefícios relativos de desempenho de cursores implícitos e explícitos com operações em massa que entram em mais detalhes sobre essas rugas específicas.