Wstawić po duplikacie aktualizacji w PostgreSQL?


644

Kilka miesięcy temu nauczyłem się z odpowiedzi na temat przepełnienia stosu, jak wykonywać wiele aktualizacji jednocześnie w MySQL przy użyciu następującej składni:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

Teraz przełączyłem się na PostgreSQL i najwyraźniej nie jest to poprawne. Odnosi się do wszystkich poprawnych tabel, więc zakładam, że chodzi o użycie różnych słów kluczowych, ale nie jestem pewien, gdzie w dokumentacji PostgreSQL jest to uwzględnione.

Aby to wyjaśnić, chcę wstawić kilka rzeczy i jeśli już istnieją, aby je zaktualizować.


38
Każdy, kto znajdzie to pytanie, powinien przeczytać artykuł Depesz'a „Dlaczego upsert jest taki skomplikowany?” . Bardzo dobrze wyjaśnia problem i możliwe rozwiązania.
Craig Ringer

8
UPSERT zostanie dodany w Postgres 9.5: wiki.postgresql.org/wiki/…
tommed

4
@tommed - zostało to zrobione: stackoverflow.com/a/34639631/4418
warren

Odpowiedzi:


515

PostgreSQL od wersji 9.5 ma składnię UPSERT z klauzulą ON CONFLICT . o następującej składni (podobnej do MySQL)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

Wyszukiwanie w archiwach grup e-mailowych postgresql hasła „upsert” prowadzi do znalezienia przykładu robienia tego, co prawdopodobnie chcesz zrobić, w instrukcji :

Przykład 38-2. Wyjątki od UPDATE / INSERT

W tym przykładzie użyto obsługi wyjątków, aby odpowiednio wykonać UPDATE lub INSERT:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        -- note that "a" must be unique
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

Być może istnieje przykład, jak to zrobić zbiorczo, używając CTE w wersji 9.1 i wyższej, na liście mailingowej hakerów :

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

Zobacz odpowiedź a_horse_with_no_name jest dla wyraźniejszego przykład.


7
Jedyne, co mi się nie podoba w tym, to to, że byłoby znacznie wolniejsze, ponieważ każdy upsert byłby własnym wywołaniem do bazy danych.
baash05

@ baash05 może istnieć sposób, aby to zrobić zbiorczo, zobacz moją zaktualizowaną odpowiedź.
Stephen Denne

2
Jedyną rzeczą, którą zrobiłbym inaczej, było użycie FOR LOU 1..2 zamiast tylko PĘTLI, aby w przypadku naruszenia jakiegoś innego wyjątkowego ograniczenia nie obracało się ono w nieskończoność.
olamork

2
Czego excludeddotyczy pierwsze rozwiązanie tutaj?
ichbinallen,

2
@ichbinallen w dokumentach Klauzule SET i WHERE w ON CONFLICT DO UPDATE mają dostęp do istniejącego wiersza przy użyciu nazwy tabeli (lub aliasu) oraz do wierszy proponowanych do wstawienia przy użyciu specjalnej tabeli wykluczonej . W takim przypadku specjalna excludedtabela zapewnia dostęp do wartości, które próbujesz WSTAWIĆ.
TMichel

429

Ostrzeżenie: nie jest to bezpieczne, jeśli jest wykonywane z wielu sesji jednocześnie (patrz zastrzeżenia poniżej).


Innym sprytnym sposobem wykonania „UPSERT” w postgresql jest zrobienie dwóch sekwencyjnych instrukcji UPDATE / INSERT, z których każda ma na celu odniesienie sukcesu lub brak efektu.

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

AKTUALIZACJA powiedzie się, jeśli wiersz o identyfikatorze „id = 3” już istnieje, w przeciwnym razie nie będzie miał wpływu.

INSERT powiedzie się tylko wtedy, gdy wiersz o identyfikatorze „id = 3” jeszcze nie istnieje.

Możesz połączyć te dwa w jeden ciąg i uruchomić je oba za pomocą pojedynczej instrukcji SQL wykonanej z aplikacji. Zdecydowanie zalecane jest uruchomienie ich razem w jednej transakcji.

Działa to bardzo dobrze, gdy jest uruchamiany w izolacji lub na zablokowanej tabeli, ale podlega warunkom wyścigu, co oznacza, że ​​może nadal zawieść z duplikatem błędu klucza, jeśli wiersz jest wstawiany jednocześnie lub może zakończyć się bez wstawienia wiersza, gdy wiersz jest usuwany jednocześnie . SERIALIZABLETransakcja na PostgreSQL 9.1 lub wyższy będzie go obsługiwać niezawodnie kosztem bardzo wysoki wskaźnik awaryjności serializacji, czyli trzeba będzie ponowić dużo. Zobacz, dlaczego upsert jest tak skomplikowany , co omawia ten przypadek bardziej szczegółowo.

Podejście to podlegaread committedinsertupdate także utraconym aktualizacjom w oderwaniu, chyba że aplikacja sprawdzi, czy wiersz, którego dotyczy luka, i zweryfikuje, czy wiersz lub wiersz, którego dotyczy problem .


6
Krótka odpowiedź: jeśli rekord istnieje, INSERT nic nie robi. Długa odpowiedź: WYBÓR w WSTAWCE zwróci tyle wyników, ile jest dopasowań klauzuli where. To co najwyżej jeden (jeśli numer jeden nie jest wynikiem podselekcji), w przeciwnym razie zero. INSERT doda zatem jeden lub zero wierszy.
Peter Becker

3
istnieje część „gdzie”, którą można uprościć za pomocą:... where not exists (select 1 from table where id = 3);
Endy Tjahjono

1
to powinna być właściwa odpowiedź .. z kilkoma drobnymi poprawkami, może być wykorzystana do masowej aktualizacji .. Humm .. Zastanawiam się, czy można użyć tabeli temp.
baash05

1
@keaplogik, że ograniczenie 9.1 dotyczy zapisywalnego CTE (wspólne wyrażenia tabelowe), które jest opisane w innej odpowiedzi. Składnia użyta w tej odpowiedzi jest bardzo prosta i od dawna obsługiwana.
bydło

8
Uwaga, ten podlega aktualizacji w utraconych read committedizolacji chyba twoi aplikacja sprawdza, aby upewnić się, że insertalbo updatemają niezerową rowcount. Zobacz dba.stackexchange.com/q/78510/7788
Craig Ringer

227

W PostgreSQL 9.1 można to osiągnąć za pomocą zapisywalnego CTE ( wspólne wyrażenie tabelowe ):

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

Zobacz te wpisy na blogu:


Pamiętaj, że to rozwiązanie nie zapobiega unikatowemu naruszeniu klucza, ale nie jest podatne na utracone aktualizacje.
Zobacz kontynuację Craig Ringer na dba.stackexchange.com


1
@ FrançoisBeausoleil: szansa na wyścig jest znacznie mniejsza niż w przypadku podejścia „spróbuj /
obsłuż

2
@ a_horse_with_no_name Co dokładnie oznacza, że ​​szansa na warunki wyścigowe jest znacznie mniejsza? Kiedy wykonuję to zapytanie jednocześnie z tymi samymi rekordami, pojawia się błąd „duplikat wartości klucza narusza unikalne ograniczenie” 100% razy, dopóki zapytanie nie wykryje, że rekord został wstawiony. Czy to kompletny przykład?
Jeroen van Dijk

4
@a_horse_with_no_name Twoje rozwiązanie wydaje się działać w równoległych sytuacjach, gdy otaczasz instrukcję upsert następującą blokadą: BEGIN WORK; ZABLOKUJ TABELĘ mytable W WYŁĄCZNYM TRYBIE WIERSZA; <UPSERT TUTAJ>; ZOBACZ PRACĘ;
Jeroen van Dijk

2
@JeroenvanDijk: dzięki. Miałem na myśli „znacznie mniejszy” to to, że jeśli kilka transakcji (i dokonaj zmiany!), Czas między aktualizacją a wstawką jest mniejszy, ponieważ wszystko jest tylko jedną instrukcją. Zawsze możesz wygenerować naruszenie pk za pomocą dwóch niezależnych instrukcji INSERT. Jeśli zablokujesz cały stół, skutecznie zserializujesz cały dostęp do niego (coś, co można osiągnąć również przy poziomie izolacji możliwym do serializacji).
a_horse_w_no_name

12
To rozwiązanie podlega utracie aktualizacji, jeśli transakcja wstawiania zostanie wycofana; nie ma żadnej kontroli, aby wymusić UPDATEwpływ na dowolne wiersze.
Craig Ringer

132

W PostgreSQL 9.5 i nowszych możesz używać INSERT ... ON CONFLICT UPDATE.

Zobacz dokumentację .

MySQL INSERT ... ON DUPLICATE KEY UPDATEmożna bezpośrednio przekształcić w a ON CONFLICT UPDATE. Nie ma też standardowej składni SQL, oba są rozszerzeniami specyficznymi dla bazy danych. MERGEku temu dobre powody , nowa składnia nie została stworzona tylko dla zabawy. (Składnia MySQL ma również problemy, które oznaczają, że nie została przyjęta bezpośrednio).

np. podana konfiguracja:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

zapytanie MySQL:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

staje się:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

Różnice:

  • Państwo musi podać nazwę kolumny (lub unikalną nazwę wiązania) do wykorzystania przy sprawdzaniu niepowtarzalności. To jestON CONFLICT (columnname) DO

  • SETNależy użyć słowa kluczowego , jakby to było normalne UPDATEzdanie

Ma też kilka fajnych funkcji:

  • Możesz mieć na sobie WHEREklauzulę UPDATE(pozwalającą ci efektywnie zamieniać ON CONFLICT UPDATEsię ON CONFLICT IGNOREw określone wartości)

  • Wartości proponowane do wstawienia są dostępne jako zmienna wierszowa EXCLUDED, która ma taką samą strukturę jak tabela docelowa. Możesz uzyskać oryginalne wartości w tabeli, używając nazwy tabeli. Tak więc w tym przypadku EXCLUDED.cbędzie 10(ponieważ to właśnie próbowaliśmy wstawić) i "table".cbędzie tak, 3ponieważ jest to bieżąca wartość w tabeli. Możesz użyć jednego lub obu w SETwyrażeniach i WHEREklauzulach.

Aby uzyskać informacje na temat upsert, zobacz Jak UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) w PostgreSQL?


Zajrzałem do rozwiązania PostgreSQL 9.5, jak opisano powyżej, ponieważ miałem luki w polu automatycznego przyrostu, gdy znajdowałem się pod MySQL ON DUPLICATE KEY UPDATE. Pobrałem Postgres 9.5 i zaimplementowałem Twój kod, ale dziwnie ten sam problem występuje w Postgres: pole szeregowe klucza podstawowego nie jest kolejne (między wstawkami a aktualizacjami są luki). Masz pomysł, co się tutaj dzieje? Czy to normalne? Masz pomysł, jak uniknąć tego zachowania? Dziękuję Ci.
WM

@WM Jest to dość nieodłączne od operacji upsert. Musisz ocenić funkcję, która generuje sekwencję przed próbą wstawienia. Ponieważ takie sekwencje są zaprojektowane do współbieżnego działania, są zwolnione z normalnej semantyki transakcji, ale nawet jeśli nie były, generacja nie jest wywoływana w podtransakcji i wycofywana, to kończy się normalnie i wykonuje resztę operacji. Tak by się stało nawet w przypadku implementacji sekwencji bez przerw. Jedynym sposobem, w jaki DB mógłby tego uniknąć, byłoby opóźnienie oceny generowania sekwencji do momentu sprawdzenia klucza.
Craig Ringer

1
@WM, który stworzyłby własne problemy. Zasadniczo utknąłeś. Ale jeśli polegasz na tym, że serial / auto_increment jest bez przerwy, masz już błędy. Można mieć luki sekwencji ze względu na cofanie w tym przemijające błędów - restartów pod obciążeniem, błędów klienckich połowie transakcji, awarie itp Musisz nigdy, nigdy polegać na SERIAL/ SEQUENCElub AUTO_INCREMENTnie posiadające luki. Jeśli potrzebujesz sekwencji bez przerw, są one bardziej złożone; zazwyczaj musisz użyć tabeli liczników. Google powie Ci więcej. Należy jednak pamiętać, że sekwencje bez przerw zapobiegają wszelkiej współbieżności wstawiania.
Craig Ringer

@WM Jeśli bezwzględnie potrzebujesz sekwencji bez przerw i wstawiania, możesz użyć opartej na funkcjach metody wstawiania omówionej w podręczniku wraz z implementacją sekwencji bez przerw, która wykorzystuje tabelę liczników. Ponieważ BEGIN ... EXCEPTION ...przebiega w podtransakcji, która jest przywracana po błędzie, przyrost sekwencji zostanie cofnięty, jeśli się INSERTnie powiedzie.
Craig Ringer

Dziękuję bardzo @Craig Ringer, to było bardzo pouczające. Uświadomiłem sobie, że mogę po prostu zrezygnować z posiadania klucza podstawowego automatycznego przyrostu. Zrobiłem kompozytowy pierwiastek z 3 pól i dla mojej konkretnej bieżącej potrzeby naprawdę nie ma potrzeby stosowania bezprzerwowego pola automatycznego przyrostu. Jeszcze raz dziękuję, informacje, które podałeś, pozwoliłyby mi zaoszczędzić czas w przyszłości, próbując zapobiec naturalnemu i zdrowemu zachowaniu DB. Teraz to rozumiem lepiej.
WM

17

Szukałem tego samego, kiedy tu przyjechałem, ale brak ogólnej funkcji „upsert” trochę mnie niepokoi, więc pomyślałem, że możesz po prostu przekazać aktualizację i wstawić sql jako argumenty tej funkcji z instrukcji

wyglądałoby to tak:

CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

i być może, aby zrobić to, co początkowo chciałeś zrobić, wsadowo „upsert”, możesz użyć Tcl, aby podzielić sql_update i zapętlić poszczególne aktualizacje, trafienie wydajności będzie bardzo małe, patrz http://archives.postgresql.org/pgsql- performance / 2006-04 / msg00557.php

najwyższy koszt to wykonanie zapytania z twojego kodu, po stronie bazy danych koszt wykonania jest znacznie niższy


3
Nadal musisz uruchamiać to w pętli ponownych prób i jest on podatny na wyścigi z równoczesną konfiguracją, DELETEchyba że zablokujesz tabelę lub nie będziesz w SERIALIZABLEizolacji transakcji na PostgreSQL 9.1 lub nowszym.
Craig Ringer

13

Nie ma na to prostego polecenia.

Najbardziej poprawnym podejściem jest użycie funkcji, takiej jak ta z dokumentów .

Innym rozwiązaniem (choć nie tak bezpiecznym) jest wykonanie aktualizacji ze zwróceniem, sprawdzenie, które wiersze były aktualizacjami, i wstawienie pozostałych

Coś w stylu:

update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;

przy założeniu, że zwrócono 2:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

Oczywiście prędzej czy później wyskoczy (w równoczesnym środowisku), ponieważ jest tu wyraźny warunek wyścigu, ale zwykle będzie działał.

Oto dłuższy i bardziej wyczerpujący artykuł na ten temat .


1
Jeśli używasz tej opcji, sprawdź, czy identyfikator jest zwracany, nawet jeśli aktualizacja nic nie robi. Widziałem, jak bazy danych optymalizują zapytania takie jak „Aktualizuj tabelę foo set bar = 4 gdzie bar = 4”.
thelem

10

Osobiście skonfigurowałem „regułę” dołączoną do instrukcji insert. Załóżmy, że masz tabelę „dns”, która rejestruje liczbę trafień dns na klienta według czasu:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

Chcesz móc ponownie wstawiać wiersze ze zaktualizowanymi wartościami lub tworzyć je, jeśli jeszcze nie istniały. Wprowadzono identyfikator klienta i godzinę. Coś takiego:

CREATE RULE replace_dns AS 
    ON INSERT TO dns 
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") 
            AND (dns.customer_id = new.customer_id)))) 
    DO INSTEAD UPDATE dns 
        SET hits = new.hits 
        WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));

Aktualizacja: może się to nie powieść, jeśli wystąpią jednoczesne wstawienia, ponieważ wygeneruje wyjątki wyjątków_nieprzestrzeganie. Jednak transakcja, która nie zostanie zakończona, będzie kontynuowana i zakończy się sukcesem, wystarczy powtórzyć zakończoną transakcję.

Jednakże, jeśli przez cały czas dzieje się mnóstwo wstawek, będziesz chciał umieścić blokadę tabeli wokół instrukcji wstawiania: blokowanie SHARE ROW EXCLUSIVE zapobiegnie wszelkim operacjom, które mogłyby wstawiać, usuwać lub aktualizować wiersze w tabeli docelowej. Jednak aktualizacje, które nie aktualizują unikatowego klucza, są bezpieczne, więc jeśli żadna operacja tego nie zrobi, użyj zamiast tego blokad doradczych.

Ponadto polecenie KOPIUJ nie używa ZASAD, więc jeśli wstawiasz za pomocą KOPIOWANIA, musisz zamiast tego użyć wyzwalaczy.


9

Używam tej funkcji scalania

CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = key)
        THEN
            UPDATE tabla SET b = data WHERE a = key;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (key, data);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql

1
Bardziej wydajne jest wykonanie updatepierwszego, a następnie sprawdzenie liczby zaktualizowanych wierszy. (Zobacz odpowiedź Ahmada)
a_horse_w_no_name

8

Niestandardową funkcję „wstawiania” powyżej, jeśli chcesz WSTAWIĆ I WYMIENIĆ:

`

 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update; 
    IF FOUND THEN 
        RETURN; 
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

A po wykonaniu wykonaj coś takiego:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

Ważne jest, aby wstawić podwójny przecinek w celu uniknięcia błędów kompilatora

  • sprawdź prędkość ...

7

Podobne do najbardziej lubianej odpowiedzi, ale działa nieco szybciej:

WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(źródło: http://www.the-art-of-web.com/sql/upsert/ )


3
Nie powiedzie się to, jeśli zostanie uruchomione jednocześnie w dwóch sesjach, ponieważ żadna aktualizacja nie wyświetli istniejącego wiersza, więc obie aktualizacje trafią w zero wierszy, więc oba zapytania spowodują wstawienie.
Craig Ringer

6

Mam ten sam problem z zarządzaniem ustawieniami konta, co pary wartości nazwa. Kryteria projektowe są takie, że różni klienci mogą mieć różne zestawy ustawień.

Moje rozwiązanie, podobnie jak JWP, polega na masowym usuwaniu i zastępowaniu, generując rekord scalania w Twojej aplikacji.

Jest to dość kuloodporne, niezależne od platformy, a ponieważ nigdy nie ma więcej niż około 20 ustawień na klienta, są to tylko 3 dość mało wywołania db db - prawdopodobnie najszybsza metoda.

Alternatywą aktualizacji poszczególnych wierszy - sprawdzania wyjątków, a następnie wstawiania - lub jakiejś kombinacji jest ohydny kod, powolny i często psuje się, ponieważ (jak wspomniano powyżej) niestandardowa obsługa wyjątków SQL zmienia się z db na db - lub nawet z wydania na wydanie.

 #This is pseudo-code - within the application:
 BEGIN TRANSACTION - get transaction lock
 SELECT all current name value pairs where id = $id into a hash record
 create a merge record from the current and update record
  (set intersection where shared keys in new win, and empty values in new are deleted).
 DELETE all name value pairs where id = $id
 COPY/INSERT merged records 
 END TRANSACTION

Witamy w SO. Niezłe wprowadzenie! :-)
Don Pytanie

1
To bardziej przypomina REPLACE INTOniż INSERT INTO ... ON DUPLICATE KEY UPDATE, co może powodować problem, jeśli użyjesz wyzwalaczy. Skończysz usuwanie i wstawianie wyzwalaczy / reguł zamiast aktualizować je.
cHao


5
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
  RETURNS boolean AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN true;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT

5

Do łączenia małych zestawów dobrze jest użyć powyższej funkcji. Jeśli jednak scalasz duże ilości danych, sugeruję zajrzenie na http://mbk.projects.postgresql.org

Obecna najlepsza praktyka, o której wiem, to:

  1. KOPIUJ nowe / zaktualizowane dane do tabeli temp (oczywiście, lub możesz zrobić WSTAW, jeśli koszt jest w porządku)
  2. Acquire Lock [opcjonalnie] (doradztwo jest lepsze niż zamki stołowe, IMO)
  3. Łączyć. (część zabawy)

5

UPDATE zwróci liczbę zmodyfikowanych wierszy. Jeśli używasz JDBC (Java), możesz następnie sprawdzić tę wartość na 0 i, jeśli nie wpłynie to na żadne wiersze, uruchom zamiast tego INSERT. Jeśli używasz innego języka programowania, być może nadal można uzyskać liczbę zmodyfikowanych wierszy, sprawdź dokumentację.

To może nie być tak eleganckie, ale masz znacznie prostszy SQL, który jest bardziej trywialny w użyciu z kodu wywołującego. Inaczej, jeśli napiszesz skrypt dziesięcioliniowy w PL / PSQL, prawdopodobnie powinieneś mieć test jednostkowy tego lub innego rodzaju tylko dla niego samego.


4

Edycja: To nie działa zgodnie z oczekiwaniami. W przeciwieństwie do przyjętej odpowiedzi, powoduje to unikalne naruszenia klucza, gdy dwa procesy wielokrotnie się wywołująupsert_foo jednocześnie.

Eureka! Wymyśliłem sposób na zrobienie tego w jednym zapytaniu: użyj, UPDATE ... RETURNINGaby sprawdzić, czy dotyczy to któregokolwiek wiersza:

CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
    UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;

CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
    INSERT INTO foo
        SELECT $1, $2
        WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;

Należy UPDATEto zrobić w oddzielnej procedurze, ponieważ niestety jest to błąd składniowy:

... WHERE NOT EXISTS (UPDATE ...)

Teraz działa zgodnie z oczekiwaniami:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');

1
Możesz połączyć je w jedną instrukcję, jeśli używasz CTE z możliwością zapisu. Ale podobnie jak większość opublikowanych tutaj rozwiązań, to jest nieprawidłowe i zawiedzie w przypadku jednoczesnych aktualizacji.
Craig Ringer
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.