Oracle: Wie UPSERT (aktualisieren oder in eine Tabelle einfügen?)

293

Die UPSERT-Operation aktualisiert oder fügt eine Zeile in eine Tabelle ein, je nachdem, ob die Tabelle bereits eine Zeile enthält, die mit den Daten übereinstimmt:

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

Wie geht das am besten, da Oracle keine bestimmte UPSERT-Anweisung hat?

Mark Harrison
quelle

Antworten:

60

Eine Alternative zu MERGE (der "altmodische Weg"):

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;   
Tony Andrews
quelle
3
@chotchki: wirklich? Eine Erklärung wäre hilfreich.
Tony Andrews
15
Das Problem ist, dass sich zwischen dem Einfügen und dem Update ein Fenster befindet, in dem ein anderer Prozess einen Löschvorgang erfolgreich auslösen kann. Ich habe dieses Muster jedoch für eine Tabelle verwendet, für die niemals Löschvorgänge ausgelöst wurden.
Chotchki
2
Ok, ich stimme zu. Ich weiß nicht, warum es mir nicht klar war.
Tony Andrews
4
Ich bin mit Chotchki nicht einverstanden. "Sperrdauer: Alle durch Anweisungen innerhalb einer Transaktion erworbenen Sperren werden für die Dauer der Transaktion gehalten, wodurch destruktive Interferenzen wie fehlerhafte Lesevorgänge, verlorene Aktualisierungen und destruktive DDL-Vorgänge bei gleichzeitigen Transaktionen verhindert werden." Quelle: Link
Yohannc
5
@yohannc: Ich denke, der Punkt ist, dass wir keine Sperren erhalten haben, nur indem wir versucht haben und es nicht geschafft haben, eine Zeile einzufügen.
Tony Andrews
211

Die MERGE-Anweisung führt Daten zwischen zwei Tabellen zusammen. Mit DUAL können wir diesen Befehl verwenden. Beachten Sie, dass dies nicht gegen gleichzeitigen Zugriff geschützt ist.

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
Mark Harrison
quelle
57
Anscheinend ist die "Zusammenführen in" -Anweisung nicht atomar. Bei gleichzeitiger Verwendung kann dies zu "ORA-0001: eindeutige Einschränkung" führen. Die Überprüfung auf das Vorhandensein einer Übereinstimmung und das Einfügen eines neuen Datensatzes sind nicht durch eine Sperre geschützt, sodass eine Rennbedingung vorliegt. Um dies zuverlässig zu tun, müssen Sie diese Ausnahme abfangen und entweder die Zusammenführung erneut ausführen oder stattdessen ein einfaches Update durchführen. In Oracle 10 können Sie die Klausel "Protokollfehler" verwenden, um mit den restlichen Zeilen fortzufahren, wenn ein Fehler auftritt, und die fehlerhafte Zeile in einer anderen Tabelle zu protokollieren, anstatt nur anzuhalten.
Tim Sylvester
1
Hallo, ich habe versucht, dasselbe Abfragemuster in meiner Abfrage zu verwenden, aber irgendwie fügt meine Abfrage doppelte Zeilen ein. Ich kann keine weiteren Informationen zur DUAL-Tabelle finden. Kann mir bitte jemand sagen, wo ich Informationen über DUAL und auch über die Zusammenführungssyntax erhalten kann?
Shekhar
5
@ Shekhar Dual ist eine Dummy-Tabelle mit einer einzelnen Zeile und Spalte adp-gmbh.ch/ora/misc/dual.html
YogoZuno
7
@TimSylvester - Oracle verwendet Transaktionen und garantiert so, dass die Momentaufnahme der Daten zu Beginn einer Transaktion während der gesamten Transaktion konsistent ist, außer dass darin vorgenommene Änderungen vorgenommen werden. Gleichzeitige Aufrufe der Datenbank verwenden den Rückgängig-Stapel. Daher verwaltet Oracle den endgültigen Status in der Reihenfolge, in der die gleichzeitigen Transaktionen gestartet / abgeschlossen wurden. Sie haben also nie eine Race-Bedingung, wenn vor dem Einfügen eine Einschränkungsprüfung durchgeführt wird, unabhängig davon, wie viele gleichzeitige Aufrufe an denselben SQL-Code erfolgen. Im schlimmsten Fall kann es zu heftigen Konflikten kommen, und Oracle benötigt viel länger, um einen endgültigen Status zu erreichen.
Neo
2
@RandyMagruder Ist es so, dass es 2015 ist und wir in Oracle immer noch keinen zuverlässigen Upsert durchführen können? Kennen Sie eine gleichzeitig sichere Lösung?
Dan B
105

Das Doppelbeispiel oben in PL / SQL war großartig, weil ich etwas Ähnliches tun wollte, aber ich wollte es clientseitig ... also hier ist das SQL, mit dem ich eine ähnliche Anweisung direkt von einem C # gesendet habe

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" )

Aus C # -Perspektive ist dies jedoch langsamer als das Aktualisieren und das Überprüfen, ob die betroffenen Zeilen 0 waren, und das Einfügen, falls dies der Fall war.

MyDeveloperDay
quelle
10
Ich bin hierher zurückgekommen, um dieses Muster noch einmal zu überprüfen. Es schlägt stillschweigend fehl, wenn gleichzeitig Einfügungen versucht werden. Eine Einfügung wird wirksam, die zweite Zusammenführung weder Einfügungen noch Aktualisierungen. Der schnellere Ansatz, zwei separate Anweisungen auszuführen, ist jedoch sicher.
Synesso
3
oralcle Neulingen wie ich kann fragen , was das ist Dual - Tabelle sehen Sie: stackoverflow.com/q/73751/808698
Hajo Thelen
5
Schade, dass wir mit diesem Muster doppelt so viele Daten schreiben müssen (John, Smith ...). In diesem Fall gewinne ich nichts mit , und ich ziehe mit viel einfacher dann . MERGEDELETEINSERT
Nicolas Barbulesco
@NicolasBarbulesco Diese Antwort muss die Daten nicht zweimal schreiben: 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

Eine weitere Alternative ohne Ausnahmeprüfung:

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;
Brian Schmitt
quelle
Ihre bereitgestellte Lösung funktioniert bei mir nicht. Funktioniert% rowcount nur mit expliziten Cursorn?
Synesso
Was ist, wenn das Update 0 Zeilen zurückgegeben hat, die geändert wurden, weil der Datensatz bereits vorhanden war und die Werte gleich waren?
Adriano Varoli Piazza
10
@Adriano: sql% rowcount gibt immer noch> 0 zurück, wenn die WHERE-Klausel mit Zeilen übereinstimmt, auch wenn das Update keine Daten in diesen Zeilen ändert.
Tony Andrews
Funktioniert nicht: PLS-00207: Der Bezeichner 'COUNT', der auf implizites Cursor-SQL angewendet wird, ist kein legales Cursorattribut
Patrik Beck
Syntaxfehler hier :(
Ilmirons
27
  1. einfügen, falls nicht vorhanden
  2. aktualisieren:
    
INSERT IN mytable (id1, t1) 
  WÄHLEN SIE 11, 'x1' AUS DOPPEL 
  WO NICHT EXISTIERT (SELECT id1 FROM mytble WHERE id1 = 11); 

UPDATE mytable SET t1 = 'x1' WHERE id1 = 11;
test1
quelle
26

Keine der bisher gegebenen Antworten ist angesichts gleichzeitiger Zugriffe sicher , wie in Tim Sylvesters Kommentar ausgeführt, und wird im Falle von Rennen Ausnahmen auslösen. Um dies zu beheben, muss die Einfüge- / Aktualisierungskombination in eine Art Schleifenanweisung eingeschlossen werden, damit im Falle einer Ausnahme das Ganze erneut versucht wird.

Als Beispiel sehen Sie, wie der Code von Grommit in eine Schleife eingeschlossen werden kann, um die Sicherheit bei gleichzeitiger Ausführung zu gewährleisten:

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 Im Transaktionsmodus SERIALIZABLE, den ich übrigens nicht empfehle, kann ORA-08177 auftreten: Der Zugriff für diese Transaktionsausnahmen kann stattdessen nicht serialisiert werden.

Eugene Beresovsky
quelle
3
Ausgezeichnet! Schließlich greift eine gleichzeitige sichere Antwort zu. Gibt es eine Möglichkeit, ein solches Konstrukt von einem Client aus zu verwenden (z. B. von einem Java-Client)?
Sebien
1
Sie wollen keinen gespeicherten Prozess aufrufen? In diesem Fall können Sie auch einfach die spezifischen Java-Ausnahmen abfangen und in einer Java-Schleife erneut versuchen. In Java ist es verdammt viel praktischer als in Oracle SQL.
Eugene Beresovsky
Es tut mir leid: Ich war nicht spezifisch genug. Aber du hast den richtigen Weg verstanden. Ich bin zurückgetreten, um das zu tun, was du gerade gesagt hast. Aber ich bin nicht 100% zufrieden, weil es mehr SQL-Abfragen und mehr Client / Server-Roundtrips generiert. In Bezug auf die Leistung ist dies keine gute Lösung. Mein Ziel ist es jedoch, dass die Java-Entwickler meines Projekts meine Methode zum Upsert in einer beliebigen Tabelle verwenden (ich kann nicht eine gespeicherte PLSQL-Prozedur pro Tabelle oder eine Prozedur pro Upsert-Typ erstellen).
Sebien
@Sebien Ich stimme zu, es wäre schöner, wenn es im SQL-Bereich gekapselt wäre, und ich denke, Sie können es tun. Ich melde mich einfach nicht freiwillig, um es für Sie herauszufinden ... :) Außerdem werden diese Ausnahmen in Wirklichkeit wahrscheinlich weniger als einmal bei einem blauen Mond auftreten, sodass Sie in 99,9% der Fälle keinen Einfluss auf die Leistung sehen sollten. Außer natürlich beim Testen der Last ...
Eugene Beresovsky
24

Ich möchte, dass Grommit antwortet, außer dass es betrogene Werte erfordert. Ich habe eine Lösung gefunden, bei der sie möglicherweise einmal angezeigt wird: 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); 
Hubbitus
quelle
2
Meinten Sie INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Matteo
Sicher. Vielen Dank. Fest.
Hubbitus
Zum Glück haben Sie Ihre Antwort bearbeitet! :) Meine Bearbeitung wurde leider abgelehnt stackoverflow.com/review/suggested-edits/7555674
Matteo
9

Ein Hinweis zu den beiden Lösungen, die Folgendes vorschlagen:

1) Einfügen, wenn Ausnahme, dann aktualisieren,

oder

2) Aktualisieren, wenn sql% rowcount = 0, dann einfügen

Die Frage, ob zuerst eingefügt oder aktualisiert werden soll, ist ebenfalls anwendungsabhängig. Erwarten Sie mehr Beilagen oder mehr Updates? Derjenige, der am wahrscheinlichsten Erfolg hat, sollte zuerst gehen.

Wenn Sie den falschen auswählen, erhalten Sie eine Reihe unnötiger Indexlesevorgänge. Keine große Sache, aber dennoch etwas zu beachten.

AnthonyVO
quelle
sql% notfound ist meine persönliche Präferenz
Arturo Hernandez
8

Ich verwende seit Jahren das erste Codebeispiel. Beachten Sie nicht gefunden, anstatt zu zählen.

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;

Der folgende Code ist der möglicherweise neue und verbesserte Code

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)

Im ersten Beispiel führt das Update eine Indexsuche durch. Es muss, um die rechte Zeile zu aktualisieren. Oracle öffnet einen impliziten Cursor und verwendet ihn, um eine entsprechende Einfügung zu verpacken, damit wir wissen, dass die Einfügung nur erfolgt, wenn der Schlüssel nicht vorhanden ist. Die Einfügung ist jedoch ein unabhängiger Befehl und muss eine zweite Suche durchführen. Ich kenne das Innenleben des Befehls merge nicht, aber da der Befehl eine einzelne Einheit ist, hätte Oracle die korrekte Einfügung oder Aktualisierung mit einer einzelnen Indexsuche ausführen können.

Ich denke, Zusammenführen ist besser, wenn Sie eine Verarbeitung durchführen müssen, dh Daten aus einigen Tabellen entnehmen und eine Tabelle aktualisieren, möglicherweise Zeilen einfügen oder löschen. Für den Fall einer einzelnen Zeile können Sie jedoch den ersten Fall in Betracht ziehen, da die Syntax häufiger verwendet wird.

Arturo Hernandez
quelle
0

Kopieren und Einfügen eines Beispiels zum Einfügen einer Tabelle in eine andere mit 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

Ergebnis:

  1. b 4 5
  2. c 3 3
  3. a 1 1
Bechyňák Petr
quelle
-3

Versuche dies,

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
)
;
r4bitt
quelle
-6

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

"In Oracle9i kann ein UPSERT diese Aufgabe in einer einzigen Anweisung ausführen:"

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;
Anon
quelle
14
-1 Typisch Don Burleson cr @ p Ich fürchte - dies ist eine Einfügung in die eine oder andere Tabelle, hier gibt es kein "Upsert"!
Tony Andrews