Oracle: come UPSERT (aggiornare o inserire in una tabella?)
Domanda
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...
Dato che Oracle non ha una specifica dichiarazione UPSERT, qual è il modo migliore per farlo?
Soluzione
Un'alternativa a MERGE (la "vecchia maniera"):
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;
Altri suggerimenti
La Dichiarazione MERGE unisce i dati tra due tavoli. Utilizzando DUAL ci permette di usare questo comando. Si noti che questo non è protetto dall'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
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 più lento rispetto all'aggiornamento e alla visualizzazione se le righe interessate erano 0 e all'inserimento se lo fosse.
Un'altra alternativa senza 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;
- inserisci se non esiste
- aggiornamento:
INSERT INTO mytable (id1, t1) SELECT 11, 'x1' FROM DUAL WHERE NOT EXISTS (SELECT id1 FROM mytble WHERE id1 = 11); UPDATE mytable SET t1 = 'x1' WHERE id1 = 11;
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 di 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. Nella modalità di transazione SERIALIZZABILE
, che non consiglio a proposito, potresti imbatterti
ORA-08177: non è invece possibile serializzare l'accesso per questa transazione eccezioni.
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 & amp;? 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);
Una nota relativa alle due soluzioni che suggeriscono:
1) Inserisci, se eccezione quindi aggiorna,
o
2) Aggiorna, se sql% rowcount = 0 quindi 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.
Uso il primo esempio di codice da anni. Avviso non fondato anziché 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 dell'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, puoi considerare il primo caso poiché la sintassi è più comune.
Copia & amp; incolla esempio per invertire 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:
- b 4 5
- c 3 3
- a 1 1
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
)
;
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;