Oracle
 sql >> Database >  >> RDS >> Oracle

Oracle:prestazioni Bulk Collect

All'interno di Oracle sono presenti una macchina virtuale (VM) SQL e una VM PL/SQL. Quando è necessario passare da una macchina virtuale all'altra, si sostiene il costo di un cambio di contesto. Singolarmente, questi cambiamenti di contesto sono relativamente rapidi, ma quando si esegue l'elaborazione riga per riga, possono sommarsi per rappresentare una frazione significativa del tempo impiegato dal codice. Quando utilizzi le associazioni in blocco, sposti più righe di dati da una macchina virtuale all'altra con un unico cambio di contesto, riducendo notevolmente il numero di cambiamenti di contesto, rendendo il tuo codice più veloce.

Prendi, ad esempio, un cursore esplicito. Se scrivo qualcosa del genere

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;

quindi ogni volta che eseguo il recupero, lo sono

  • Esecuzione di un cambio di contesto dalla VM PL/SQL alla VM SQL
  • Chiedere alla VM SQL di eseguire il cursore per generare la riga di dati successiva
  • Esecuzione di un altro spostamento del contesto dalla VM SQL alla VM PL/SQL per restituire la mia singola riga di dati

E ogni volta che inserisco una riga, sto facendo la stessa cosa. Sto sostenendo il costo di un cambio di contesto per spedire una riga di dati dalla VM PL/SQL alla VM SQL, chiedendo all'SQL di eseguire INSERT istruzione e quindi sostenere il costo di un altro passaggio di contesto a PL/SQL.

Se source_table ha 1 milione di righe, ovvero 4 milioni di spostamenti di contesto che probabilmente rappresenteranno una frazione ragionevole del tempo trascorso del mio codice. Se invece faccio un BULK COLLECT con un LIMIT di 100, posso eliminare il 99% dei miei spostamenti di contesto recuperando 100 righe di dati dalla VM SQL in una raccolta in PL/SQL ogni volta che sostengo il costo di un cambio di contesto e inserendo 100 righe nella tabella di destinazione ogni volta che subire un cambiamento di contesto lì.

Se posso riscrivere il mio codice per utilizzare le operazioni di 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;

Ora, ogni volta che eseguo il recupero, recupero 100 righe di dati nella mia raccolta con un unico set di cambiamenti di contesto. E ogni volta che faccio il mio FORALL inserisci, sto inserendo 100 righe con un unico set di spostamenti di contesto. Se source_table ha 1 milione di righe, questo significa che sono passato da 4 milioni di cambi di contesto a 40.000 cambiamenti di contesto. Se i cambiamenti di contesto hanno rappresentato, ad esempio, il 20% del tempo trascorso del mio codice, ho eliminato il 19,8% del tempo trascorso.

Puoi aumentare la dimensione del LIMIT per ridurre ulteriormente il numero di cambiamenti di contesto, ma hai rapidamente colpito la legge dei rendimenti decrescenti. Se hai utilizzato un LIMIT di 1000 anziché 100, elimineresti il ​​99,9% dei cambiamenti di contesto anziché il 99%. Ciò significherebbe che la tua raccolta utilizzava 10 volte più memoria PGA, tuttavia. E nel nostro ipotetico esempio eliminerebbe solo lo 0,18% in più di tempo trascorso. Raggiungi molto rapidamente un punto in cui la memoria aggiuntiva che stai utilizzando aggiunge più tempo di quanto risparmi eliminando ulteriori cambiamenti di contesto. In generale, un LIMIT tra 100 e 1000 è probabile che sia il punto debole.

Naturalmente, in questo esempio, sarebbe ancora più efficiente eliminare tutti i cambiamenti di contesto e fare tutto in un'unica istruzione SQL

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

Avrebbe senso ricorrere a PL/SQL in primo luogo solo se stai eseguendo una sorta di manipolazione dei dati dalla tabella di origine che non puoi ragionevolmente implementare in SQL.

Inoltre, ho usato intenzionalmente un cursore esplicito nel mio esempio. Se utilizzi cursori impliciti, nelle versioni recenti di Oracle ottieni i vantaggi di un BULK COLLECT con un LIMIT di 100 implicitamente. C'è un'altra domanda StackOverflow che discute i vantaggi in termini di prestazioni relative dei cursori impliciti ed espliciti con operazioni in blocco che approfondiscono maggiormente quelle particolari rughe.