Oracle: jak UPSERT (zaktualizować lub wstawić do tabeli?)

293

Operacja UPSERT aktualizuje lub wstawia wiersz do tabeli, w zależności od tego, czy tabela ma już wiersz zgodny z danymi:

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

Ponieważ Oracle nie ma określonej instrukcji UPSERT, jaki jest najlepszy sposób na to?

Mark Harrison
źródło

Odpowiedzi:

60

Alternatywa dla MERGE („staromodny sposób”):

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
źródło
3
@chotchki: naprawdę? Wyjaśnienie byłoby pomocne.
Tony Andrews,
15
Problem polega na tym, że między wstawką a aktualizacją znajduje się okno, w którym inny proces może pomyślnie uruchomić usunięcie. Użyłem jednak tego wzorca na stole, który nigdy nie został usunięty przeciwko niemu.
chotchki
2
Ok zgadzam się. Nie wiem, dlaczego nie było to dla mnie oczywiste.
Tony Andrews,
4
Nie zgadzam się z Chotchki. „Czas trwania blokady: wszystkie blokady uzyskane przez wyciągi w ramach transakcji są przechowywane przez czas trwania transakcji, zapobiegając destrukcyjnym zakłóceniom, w tym brudnym odczytom, utracie aktualizacji i destrukcyjnym operacjom DDL z jednoczesnych transakcji”. Souce: link
yohannc
5
@yohannc: Myślę, że chodzi o to, że nie uzyskaliśmy żadnych blokad tylko poprzez próbę wstawienia wiersza.
Tony Andrews,
211

Instrukcja MERGE scala dane między dwiema tabelami. Użycie DUAL pozwala nam użyć tego polecenia. Pamiętaj, że nie jest to chronione przed równoczesnym dostępem.

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
źródło
57
Najwyraźniej stwierdzenie „scal się w” nie ma charakteru atomowego. Może to powodować powstanie „ORA-0001: unikalne ograniczenie”, gdy jest używane jednocześnie. Sprawdzanie istnienia meczu i wstawienie nowego rekordu nie są chronione przez blokadę, więc występuje warunek wyścigu. Aby to zrobić niezawodnie, musisz wychwycić ten wyjątek i ponownie uruchomić scalanie lub zamiast tego wykonać prostą aktualizację. W Oracle 10 można użyć klauzuli „rejestruj błędy”, aby kontynuować pracę z resztą wierszy, gdy wystąpi błąd, i zalogować niepoprawny wiersz do innej tabeli, a nie tylko zatrzymać.
Tim Sylvester
1
Cześć, próbowałem użyć tego samego wzorca zapytania w zapytaniu, ale w jakiś sposób moje zapytanie wstawia zduplikowane wiersze. Nie mogę znaleźć więcej informacji o stole DUAL. Czy ktoś może mi powiedzieć, gdzie mogę uzyskać informacje o DUAL, a także o składni scalania?
Shekhar
5
@Shekhar Dual to fikcyjny stół z jednym wierszem i kolumną adp-gmbh.ch/ora/misc/dual.html
YogoZuno
7
@ TimSylvester - Oracle korzysta z transakcji, dzięki czemu gwarantuje, że migawka danych na początku transakcji jest spójna podczas całej transakcji, z wyjątkiem wszelkich wprowadzonych w niej zmian. Jednoczesne wywołania bazy danych wykorzystują stos cofania; więc Oracle będzie zarządzać stanem końcowym na podstawie kolejności rozpoczęcia / zakończenia jednoczesnych transakcji. Tak więc nigdy nie będziesz mieć warunków wyścigu, jeśli sprawdzanie ograniczenia zostanie wykonane przed wstawką, niezależnie od liczby jednoczesnych wywołań tego samego kodu SQL. W najgorszym przypadku możesz mieć dużo sporów, a Oracle osiągnie ostateczny stan znacznie dłużej.
Neo,
2
@RandyMagruder Czy to przypadek, że jego 2015, a my nadal nie możemy rzetelnie wykonać podwyżki w Oracle! Czy znasz jednocześnie bezpieczne rozwiązanie?
dan b
105

Podwójny przykład powyżej, który jest w PL / SQL, był świetny, ponieważ chciałem zrobić coś podobnego, ale chciałem, aby to było po stronie klienta ... więc tutaj jest SQL, który wysłałem podobną instrukcję bezpośrednio z jakiegoś 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" )

Jednak z perspektywy C # zapewnia to wolniejsze działanie niż przeprowadzanie aktualizacji i sprawdzanie, czy zmienione wiersze miały wartość 0, i wykonywanie wstawiania, jeśli tak było.

MyDeveloperDay
źródło
10
Wróciłem tutaj, aby ponownie sprawdzić ten wzór. Nie działa cicho, gdy próbuje się wstawić współbieżne. Jedna wstawka obowiązuje, a druga scalanie nie wstawia ani nie aktualizuje. Jednak szybsze podejście do dwóch osobnych instrukcji jest bezpieczne.
Synesso,
3
oralcle początkujących jak ja może zapytać co to jest podwójny stół zobaczyć to: stackoverflow.com/q/73751/808698
Hajo Thelen
5
Szkoda, że ​​przy tym wzorze musimy zapisać dwa razy dane (John, Smith ...). W tym przypadku nic nie wygrywam za pomocą MERGEi wolę używać znacznie prostszego DELETEniż INSERT.
Nicolas Barbulesco
@NicolasBarbulesco ta odpowiedź nie musi dwa razy zapisywać danych: stackoverflow.com/a/4015315/8307814
dlaczego
@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

Inna alternatywa bez kontroli wyjątku:

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
źródło
Twoje dostarczone rozwiązanie nie działa dla mnie. Czy% rowcount działa tylko z wyraźnymi kursorami?
Synesso,
Co jeśli aktualizacja zwróciła 0 zmodyfikowanych wierszy, ponieważ rekord już tam był, a wartości były takie same?
Adriano Varoli Piazza
10
@Adriano: sql% rowcount nadal zwróci> 0, jeśli klauzula WHERE pasuje do dowolnego wiersza, nawet jeśli aktualizacja tak naprawdę nie zmienia żadnych danych w tych wierszach.
Tony Andrews,
Nie działa: PLS-00207: identyfikator „COUNT” zastosowany do ukrytego kursora SQL nie jest prawnym atrybutem kursora
Patrik Beck
Błędy składniowe tutaj :(
ilmirons,
27
  1. wstaw, jeśli nie istnieje
  2. aktualizacja:
    
INSERT INTO mytable (id1, t1) 
  WYBIERZ 11, „x1” OD DUAL 
  GDZIE NIE ISTNIEJE (WYBIERZ id1 Z mytble GDZIE id1 = 11); 

AKTUALIZACJA mytable SET t1 = „x1” GDZIE id1 = 11;
test1
źródło
26

Żadna z dotychczas udzielonych odpowiedzi nie jest bezpieczna w obliczu równoczesnego dostępu , jak wskazano w komentarzu Tima Sylvester'a, i nie podniesie wyjątków w przypadku wyścigów. Aby to naprawić, zestaw wstawiania / aktualizacji musi być zawinięty w jakąś instrukcję pętli, aby w przypadku wyjątku ponowna próba została wykonana.

Oto przykład, w jaki sposób kod Grommita można owinąć w pętlę, aby był bezpieczny, gdy jest uruchamiany jednocześnie:

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; 

Uwaga: W trybie transakcji SERIALIZABLE, którego nie polecam btw, możesz wpaść na ORA-08177: zamiast tego nie możesz serializować dostępu dla wyjątków transakcji .

Eugene Beresovsky
źródło
3
Świetny! Wreszcie jednoczesny dostęp do bezpiecznej odpowiedzi. Jakikolwiek sposób na użycie takiej konstrukcji od klienta (np. Od klienta Java)?
Sebien
1
Masz na myśli, że nie musisz dzwonić do przechowywanego proc? Cóż, w takim przypadku możesz po prostu złapać określone wyjątki Java i spróbować ponownie w pętli Java. Jest o wiele wygodniejszy w Javie niż SQL Oracle.
Eugene Beresovsky,
Przepraszam: nie byłem wystarczająco dokładny. Ale zrozumiałeś właściwą drogę. Zrezygnowałem z tego, co powiedziałeś. Ale nie jestem w 100% zadowolony, ponieważ generuje więcej zapytań SQL, więcej objazdów klient / serwer. Nie jest to dobre rozwiązanie pod względem wydajności. Ale moim celem jest umożliwienie programistom Java mojego projektu użycia mojej metody do wstawiania w dowolnej tabeli (nie mogę utworzyć jednej procedury przechowywanej PLSQL na tabelę lub jednej procedury dla każdego typu wstawiania).
Sebien
@Sebien Zgadzam się, byłoby fajniej mieć enkapsulowane w dziedzinie SQL i myślę, że możesz to zrobić. Po prostu nie chcę się z tobą dowiedzieć ... :) Dodatkowo, w rzeczywistości te wyjątki prawdopodobnie wystąpią rzadziej niż raz na niebieskim księżycu, więc nie powinieneś widzieć wpływu na wydajność w 99,9% przypadków. Oczywiście z wyjątkiem testów obciążenia ...
Eugene Beresovsky
24

Chciałbym uzyskać odpowiedź Grommita, z wyjątkiem tego, że wymaga wartości duplikowania. Znalazłem rozwiązanie, w którym może się pojawić raz: 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
źródło
2
Miałeś na myśli INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Matteo,
Pewnie. Dzięki. Naprawiony.
Hubbitus
Na szczęście zredagowałeś swoją odpowiedź! :) moja edycja została odrzucona niestety stackoverflow.com/review/suggested-edits/7555674
Matteo
9

Uwaga dotycząca dwóch sugerowanych rozwiązań:

1) Wstaw, jeśli wyjątek, a następnie zaktualizuj,

lub

2) Zaktualizuj, jeśli sql% rowcount = 0, następnie wstaw

Pytanie, czy najpierw wstawić, czy zaktualizować, zależy również od aplikacji. Oczekujesz więcej wstawek lub aktualizacji? Ten, który najprawdopodobniej odniesie sukces, powinien zająć pierwsze miejsce.

Jeśli wybierzesz niewłaściwy, otrzymasz niepotrzebne odczyty indeksu. Nie jest to wielka sprawa, ale wciąż coś do rozważenia.

AnthonyVO
źródło
sql% notfound jest moją osobistą preferencją
Arturo Hernandez
8

Pierwszej próbki kodu używam od lat. Zauważ, że nie znaleziono, a nie policz.

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;

Poniższy kod jest prawdopodobnie nowym i ulepszonym kodem

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)

W pierwszym przykładzie aktualizacja wykonuje wyszukiwanie indeksu. Musi to zrobić, aby zaktualizować prawy wiersz. Oracle otwiera niejawny kursor i używamy go do zawijania odpowiedniej wstawki, abyśmy wiedzieli, że wstawka nastąpi tylko wtedy, gdy klucz nie istnieje. Ale wstawka jest niezależnym poleceniem i musi wykonać drugie wyszukiwanie. Nie znam wewnętrznego działania polecenia scalania, ale ponieważ polecenie to pojedyncza jednostka, Oracle może wykonać poprawne wstawienie lub aktualizację za pomocą pojedynczego wyszukiwania indeksu.

Myślę, że scalanie jest lepsze, gdy trzeba wykonać pewne przetwarzanie, co oznacza pobieranie danych z niektórych tabel i aktualizowanie tabeli, ewentualnie wstawianie lub usuwanie wierszy. Ale w przypadku pojedynczego wiersza można rozważyć pierwszy przypadek, ponieważ składnia jest częstsza.

Arturo Hernandez
źródło
0

Skopiuj i wklej przykład wstawiania jednej tabeli do drugiej za pomocą 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

Wynik:

  1. b 4 5
  2. c 3 3
  3. a 1 1
Bechyňák Petr
źródło
-3

Spróbuj tego,

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
źródło
-6

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

„W Oracle9i UPSERT może wykonać to zadanie za pomocą jednego polecenia:”

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;
Zaraz
źródło
14
-1 Typowy Don Burleson cr @ p Obawiam się - to jest wkładka do takiego czy innego stołu, tutaj nie ma „upsert”!
Tony Andrews,