Oracle
 sql >> Base de Dados >  >> RDS >> Oracle

Oracle:desempenho de coleta em massa


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.