Oracle: come UPSERT (aggiornare o inserire in una tabella?)


293

L'operazione UPSERT aggiorna o inserisce una riga in una tabella, a seconda che la tabella abbia già una riga che corrisponde ai dati:

if table t has a row exists that has key X:
    update t set mystuff... where mykey=X
else
    insert into t mystuff...

Poiché Oracle non ha una specifica dichiarazione UPSERT, qual è il modo migliore per farlo?

Risposte:


60

Un'alternativa a MERGE (il "vecchio stile"):

begin
   insert into t (mykey, mystuff) 
      values ('X', 123);
exception
   when dup_val_on_index then
      update t 
      set    mystuff = 123 
      where  mykey = 'X';
end;   

3
@chotchki: davvero? Una spiegazione sarebbe utile.
Tony Andrews,

15
Il problema è che c'è una finestra tra l'inserto e l'aggiornamento in cui un altro processo potrebbe generare correttamente un'eliminazione. Tuttavia, ho usato questo schema su un tavolo che non ha mai cancellato.
Chotchki,

2
Ok, sono d'accordo. Non so perché non fosse ovvio per me.
Tony Andrews,

4
Non sono d'accordo con Chotchki. "Durata blocco: tutti i blocchi acquisiti dagli estratti conto all'interno di una transazione vengono mantenuti per la durata della transazione, prevenendo interferenze distruttive tra cui letture sporche, aggiornamenti persi e operazioni DDL distruttive da transazioni simultanee." Souce: link
yohannc,

5
@yohannc: penso che il punto sia che non abbiamo acquisito alcun blocco solo provando e non riuscendo a inserire una riga.
Tony Andrews,

211

L' istruzione MERGE unisce i dati tra due tabelle. L'uso di DUAL ci consente di usare questo comando. Si noti che questo non è protetto contro l'accesso simultaneo.

create or replace
procedure ups(xa number)
as
begin
    merge into mergetest m using dual on (a = xa)
         when not matched then insert (a,b) values (xa,1)
             when matched then update set b = b+1;
end ups;
/
drop table mergetest;
create table mergetest(a number, b number);
call ups(10);
call ups(10);
call ups(20);
select * from mergetest;

A                      B
---------------------- ----------------------
10                     2
20                     1

57
Apparentemente la frase "unisci in" non è atomica. Può provocare "ORA-0001: vincolo univoco" se utilizzato contemporaneamente. Il controllo dell'esistenza di una partita e l'inserimento di un nuovo record non sono protetti da un lucchetto, quindi esiste una condizione di gara. Per fare ciò in modo affidabile, è necessario catturare questa eccezione e rieseguire l'unione o eseguire invece un semplice aggiornamento. In Oracle 10, puoi utilizzare la clausola "errori di registro" per farla continuare con il resto delle righe quando si verifica un errore e registrare la riga offensiva su un'altra tabella, anziché semplicemente arrestarla.
Tim Sylvester,

1
Ciao, ho provato a utilizzare lo stesso modello di query nella mia query, ma in qualche modo la mia query sta inserendo righe duplicate. Non sono in grado di trovare ulteriori informazioni sulla tabella DUAL. Qualcuno può dirmi dove posso ottenere informazioni su DUAL e anche sulla sintassi di unione?
Shekhar,

5
@Shekhar Dual è un tavolo fittizio con una sola riga e columnn adp-gmbh.ch/ora/misc/dual.html
YogoZuno

7
@TimSylvester - Oracle utilizza le transazioni, quindi garantisce che l'istantanea dei dati all'inizio di una transazione sia coerente durante la transazione, salvo eventuali modifiche apportate al suo interno. Le chiamate simultanee al database utilizzano lo stack di annullamento; così Oracle gestirà lo stato finale in base all'ordine di inizio / completamento delle transazioni simultanee. Quindi, non avrai mai una condizione di competizione se viene eseguito un controllo dei vincoli prima dell'inserimento, indipendentemente dal numero di chiamate simultanee effettuate allo stesso codice SQL. Nel peggiore dei casi, potresti ricevere molte controversie e Oracle impiegherà molto più tempo per raggiungere uno stato finale.
Neo

2
@RandyMagruder È forse il 2015 e non possiamo ancora fare affidamento su Oracle in modo affidabile! Conosci una soluzione sicura concomitante?
dan b

105

Il doppio esempio sopra riportato in PL / SQL è stato ottimo perché volevo fare qualcosa di simile, ma lo volevo sul lato client ... quindi ecco l'SQL che ho usato per inviare un'istruzione simile direttamente da alcuni C #

MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name") 
    VALUES ( 2097153,"smith", "john" )

Tuttavia, dal punto di vista C #, ciò risulta essere più lento rispetto all'aggiornamento e alla visualizzazione se le righe interessate erano 0 e all'inserimento se lo fosse.


10
Sono tornato qui per vedere di nuovo questo schema. Non funziona in modo silenzioso quando si tentano inserimenti simultanei. Un inserto ha effetto, il secondo non unisce né inserti né aggiornamenti. Tuttavia, l'approccio più rapido di fare due dichiarazioni separate è sicuro.
Synesso,

3
i neofiti di oralcle come me potrebbero chiedere cos'è questo doppio tavolo, vedi questo: stackoverflow.com/q/73751/808698
Hajo Thelen

5
Peccato che con questo modello dobbiamo scrivere due volte i dati (John, Smith ...). In questo caso, vinco niente usando MERGE, e io preferisco usare molto più semplice DELETEpoi INSERT.
Nicolas Barbulesco,

@NicolasBarbulesco questa risposta non ha bisogno di scrivere i dati due volte: stackoverflow.com/a/4015315/8307814
whyer

@NicolasBarbulescoMERGE INTO mytable d USING (SELECT 1 id, 'x' name from dual) s ON (d.id = s.id) WHEN MATCHED THEN UPDATE SET d.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
whyer

46

Un'altra alternativa senza il controllo delle eccezioni:

UPDATE tablename
    SET val1 = in_val1,
        val2 = in_val2
    WHERE val3 = in_val3;

IF ( sql%rowcount = 0 )
    THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;

La tua soluzione fornita non funziona per me. % Rowcount funziona solo con cursori espliciti?
Synesso,

Cosa succede se l'aggiornamento restituisce 0 righe modificate perché il record era già presente e i valori erano gli stessi?
Adriano Varoli Piazza

10
@Adriano: sql% rowcount restituirà comunque> 0 se la clausola WHERE corrisponde a una riga, anche se l'aggiornamento in realtà non modifica alcun dato su tali righe.
Tony Andrews,

Non funziona: PLS-00207: l'identificatore 'COUNT', applicato al cursore implicito SQL, non è un attributo cursore legale
Patrik Beck,

Errori di sintassi qui :(
ilmirons

27
  1. inserisci se non esiste
  2. aggiornare:
    
INSERISCI IN mytable (id1, t1) 
  SELEZIONA 11, 'x1' DA DOPPIO 
  DOVE NON ESISTE (SELEZIONARE id1 DA mito DOVE id1 = 11); 

UPDATE mytable SET t1 = 'x1' DOVE id1 = 11;

26

Nessuna delle risposte fornite finora è sicura di fronte ad accessi simultanei , come sottolineato nel commento di Tim Sylvester, e solleverà eccezioni in caso di gare. Per risolvere il problema, la combinazione di inserimento / aggiornamento deve essere racchiusa in una sorta di istruzione loop, in modo che in caso di eccezione venga riprovato il tutto.

Ad esempio, ecco come il codice Grommit può essere racchiuso in un ciclo per renderlo sicuro quando eseguito contemporaneamente:

PROCEDURE MyProc (
 ...
) IS
BEGIN
 LOOP
  BEGIN
    MERGE INTO Employee USING dual ON ( "id"=2097153 )
      WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
      WHEN NOT MATCHED THEN INSERT ("id","last","name") 
        VALUES ( 2097153,"smith", "john" );
    EXIT; -- success? -> exit loop
  EXCEPTION
    WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
      NULL; -- exception? -> no op, i.e. continue looping
    WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
      NULL; -- exception? -> no op, i.e. continue looping
  END;
 END LOOP;
END; 

NB In modalità transazione SERIALIZABLE, che non consiglio a proposito, potresti imbatterti in ORA-08177: non puoi invece serializzare l'accesso per le eccezioni di questa transazione .


3
Eccellente! Infine, un accesso simultaneo accede a una risposta sicura. Qualche modo per usare un tale costrutto da un client (es. Da un client Java)?
Sebien,

1
Intendi non dover chiamare un proc memorizzato? Bene, in quel caso potresti anche catturare le specifiche eccezioni Java e riprovare in un loop Java. È molto più conveniente in Java rispetto all'SQL di Oracle.
Eugene Beresovsky,

Mi dispiace: non ero abbastanza specifico. Ma hai capito nel modo giusto. Mi sono dimesso per fare come hai appena detto. Ma non sono soddisfatto al 100% perché genera più query SQL, più roundtrip client / server. Non è una buona soluzione dal punto di vista delle prestazioni. Ma il mio obiettivo è lasciare che gli sviluppatori Java del mio progetto utilizzino il mio metodo per eseguire l'upert in qualsiasi tabella (non riesco a creare una procedura memorizzata PLSQL per tabella o una procedura per tipo di upsert).
Sebien,

@Sebien Sono d'accordo, sarebbe meglio averlo incapsulato nel regno SQL, e penso che tu possa farlo. Non mi offro volontario per capirlo ... :) Inoltre, in realtà queste eccezioni probabilmente si verificano meno di una volta in una luna blu, quindi non dovresti vedere un impatto sulle prestazioni nel 99,9% dei casi. Tranne quando si fa il test di carico ovviamente ...
Eugene Beresovsky,

24

Vorrei che Grommit rispondesse, tranne per il fatto che richiede valori duplicati. Ho trovato una soluzione dove potrebbe apparire una volta: http://forums.devshed.com/showpost.php?p=1182653&postcount=2

MERGE INTO KBS.NUFUS_MUHTARLIK B
USING (
    SELECT '028-01' CILT, '25' SAYFA, '6' KUTUK, '46603404838' MERNIS_NO
    FROM DUAL
) E
ON (B.MERNIS_NO = E.MERNIS_NO)
WHEN MATCHED THEN
    UPDATE SET B.CILT = E.CILT, B.SAYFA = E.SAYFA, B.KUTUK = E.KUTUK
WHEN NOT MATCHED THEN
    INSERT (  CILT,   SAYFA,   KUTUK,   MERNIS_NO)
    VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); 

2
Volevi dire INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Matteo

Sicuro. Grazie. Fisso.
Hubbitus,

Per fortuna hai modificato la tua risposta! :) la mia modifica è stata purtroppo respinta stackoverflow.com/review/suggested-edits/7555674
Matteo

9

Una nota relativa alle due soluzioni che suggeriscono:

1) Inserisci, se eccezione quindi aggiorna,

o

2) Aggiorna, se sql% rowcount = 0, inserisci

La domanda se inserire o aggiornare prima dipende anche dall'applicazione. Ti aspetti più inserti o più aggiornamenti? Quello che molto probabilmente avrà successo dovrebbe andare per primo.

Se scegli quella sbagliata otterrai un sacco di letture dell'indice non necessarie. Non è un grosso problema, ma ancora qualcosa da considerare.


sql% notfound è la mia preferenza personale
Arturo Hernandez,

8

Sto usando il primo esempio di codice da anni. Avviso non fondato piuttosto che contare.

UPDATE tablename SET val1 = in_val1, val2 = in_val2
    WHERE val3 = in_val3;
IF ( sql%notfound ) THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;

Il codice seguente è forse il codice nuovo e migliorato

MERGE INTO tablename USING dual ON ( val3 = in_val3 )
WHEN MATCHED THEN UPDATE SET val1 = in_val1, val2 = in_val2
WHEN NOT MATCHED THEN INSERT 
    VALUES (in_val1, in_val2, in_val3)

Nel primo esempio l'aggiornamento esegue una ricerca dell'indice. Deve, per aggiornare la riga giusta. Oracle apre un cursore implicito e lo usiamo per avvolgere un inserto corrispondente, quindi sappiamo che l'inserimento avverrà solo quando la chiave non esiste. Ma l'inserimento è un comando indipendente e deve eseguire una seconda ricerca. Non conosco il funzionamento interno del comando di unione ma poiché il comando è una singola unità, Oracle potrebbe aver eseguito l'inserimento o l'aggiornamento corretti con una singola ricerca di indice.

Penso che l'unione sia migliore quando hai qualche elaborazione da fare, questo significa prendere i dati da alcune tabelle e aggiornare una tabella, possibilmente inserendo o eliminando le righe. Ma per il caso a riga singola, è possibile considerare il primo caso poiché la sintassi è più comune.


0

Copia e incolla l'esempio per inserire una tabella in un'altra, con MERGE:

CREATE GLOBAL TEMPORARY TABLE t1
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5)
     )
  ON COMMIT DELETE ROWS;

CREATE GLOBAL TEMPORARY TABLE t2
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5))
  ON COMMIT DELETE ROWS;
ALTER TABLE t2 ADD CONSTRAINT PK_LKP_MIGRATION_INFO PRIMARY KEY (id);

insert into t1 values ('a','1','1');
insert into t1 values ('b','4','5');
insert into t2 values ('b','2','2');
insert into t2 values ('c','3','3');


merge into t2
using t1
on (t1.id = t2.id) 
when matched then 
  update set t2.value = t1.value,
  t2.value2 = t1.value2
when not matched then
  insert (t2.id, t2.value, t2.value2)  
  values(t1.id, t1.value, t1.value2);

select * from t2

Risultato:

  1. b 4 5
  2. c 3 3
  3. a 1 1

-3

Prova questo,

insert into b_building_property (
  select
    'AREA_IN_COMMON_USE_DOUBLE','Area in Common Use','DOUBLE', null, 9000, 9
  from dual
)
minus
(
  select * from b_building_property where id = 9
)
;

-6

Da http://www.praetoriate.com/oracle_tips_upserts.htm :

"In Oracle9i, un UPSERT può eseguire questa attività in una singola istruzione:"

INSERT
FIRST WHEN
   credit_limit >=100000
THEN INTO
   rich_customers
VALUES(cust_id,cust_credit_limit)
   INTO customers
ELSE
   INTO customers SELECT * FROM new_customers;

14
-1 Tipico Don Burleson cr @ p Temo - questo è un inserto in un tavolo o nell'altro, qui non c'è "inversione"!
Tony Andrews,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.