it-swarm.com.de

Bei doppeltem Update in PostgreSQL einfügen?

Vor einigen Monaten habe ich aus einer Antwort zu Stack Overflow gelernt, wie man in MySQL mehrere Aktualisierungen gleichzeitig durchführt und dabei folgende Syntax verwendet:

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

Ich habe jetzt auf PostgreSQL umgestellt und anscheinend stimmt das nicht. Es bezieht sich auf alle korrekten Tabellen, daher gehe ich davon aus, dass unterschiedliche Schlüsselwörter verwendet werden, aber ich bin nicht sicher, wo dies in der PostgreSQL-Dokumentation behandelt wird.

Um dies zu verdeutlichen, möchte ich einige Dinge einfügen und, falls sie bereits existieren, aktualisieren.

593
Teifion

PostgreSQL hat seit Version 9.5 die Syntax UPSERT mit der Klausel ON CONFLICT . mit der folgenden Syntax (ähnlich wie 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;

Das Durchsuchen der E-Mail-Gruppenarchive von postgresql nach "upsert" führt zu dem Ergebnis ein Beispiel für das, was Sie möglicherweise tun möchten, im Handbuch :

Beispiel 38-2. Ausnahmen mit UPDATE/INSERT

In diesem Beispiel wird die Ausnahmebehandlung verwendet, um entweder UPDATE oder INSERT auszuführen:

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');

In der Hacker-Mailingliste gibt es möglicherweise ein Beispiel dafür, wie dies in großen Mengen mithilfe von CTEs in 9.1 und höher durchgeführt werden kann:

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;

Siehe Antwort von a_horse_with_no_name für ein klareres Beispiel.

449
Stephen Denne

Warnung: Dies ist nicht sicher, wenn mehrere Sitzungen gleichzeitig ausgeführt werden (siehe nachstehende Vorsichtsmaßnahmen).


Eine andere clevere Möglichkeit, ein "UPSERT" in postgresql auszuführen, besteht darin, zwei aufeinanderfolgende UPDATE/INSERT-Anweisungen auszuführen, die jeweils so konzipiert sind, dass sie erfolgreich sind oder keine Auswirkungen haben.

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

Das UPDATE ist erfolgreich, wenn bereits eine Zeile mit "id = 3" vorhanden ist, andernfalls hat es keine Auswirkung.

Das INSERT ist nur erfolgreich, wenn die Zeile mit "id = 3" noch nicht vorhanden ist.

Sie können diese beiden in einer einzigen Zeichenfolge kombinieren und beide mit einer einzigen SQL-Anweisung ausführen, die von Ihrer Anwendung ausgeführt wird. Es wird dringend empfohlen, sie zusammen in einer einzigen Transaktion auszuführen.

Dies funktioniert sehr gut, wenn es isoliert oder in einer gesperrten Tabelle ausgeführt wird, unterliegt jedoch Race-Bedingungen, die bedeuten, dass es bei gleichzeitigem Einfügen einer Zeile immer noch zu Fehlern mit doppelten Schlüsseln kommen kann oder dass beim gleichzeitigen Löschen einer Zeile keine Zeile eingefügt wird . Eine SERIALIZABLE -Transaktion unter PostgreSQL 9.1 oder höher erledigt dies zuverlässig auf Kosten einer sehr hohen Serialisierungsfehlerrate, was bedeutet, dass Sie viel wiederholen müssen. Siehe warum ist Upsert so kompliziert , das diesen Fall ausführlicher bespricht.

Dieser Ansatz ist auch vorbehaltlich verlorener Aktualisierungen in der Isolation von read committed, es sei denn, die Anwendung überprüft die Anzahl der betroffenen Zeilen und überprüft, ob entweder insert oder update eine Zeile betroffen hat .

419
bovine

Mit PostgreSQL 9.1 kann dies mit einem beschreibbaren CTE ( common table expression ) erreicht werden:

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)

Siehe diese Blogeinträge:


Beachten Sie, dass diese Lösung eine Verletzung des eindeutigen Schlüssels nicht verhindert , jedoch nicht für verlorene Updates anfällig ist.
Siehe Follow-up von Craig Ringer auf dba.stackexchange.com

223

Ab PostgreSQL 9.5 können Sie INSERT ... ON CONFLICT UPDATE verwenden.

Siehe die Dokumentation .

Ein MySQL INSERT ... ON DUPLICATE KEY UPDATE kann direkt in einen ON CONFLICT UPDATE umformuliert werden. Weder ist SQL-Standard-Syntax, sie sind beide datenbankspezifische Erweiterungen. Es gibt gute Gründe, warum MERGE nicht verwendet wurde , eine neue Syntax wurde nicht nur zum Spaß erstellt. (Die Syntax von MySQL weist auch Probleme auf, die bedeuten, dass sie nicht direkt übernommen wurde.).

z.B. vorgegebenes Setup:

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

die MySQL-Abfrage:

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

wird:

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

Unterschiede:

  • Sie müssen den Spaltennamen (oder eindeutigen Einschränkungsnamen) angeben, der für die Eindeutigkeitsprüfung verwendet werden soll. Das ist die ON CONFLICT (columnname) DO

  • Das Schlüsselwort SET muss verwendet werden, als ob dies eine normale UPDATE Anweisung wäre

Es hat auch einige nette Funktionen:

  • Sie können eine WHERE -Klausel in Ihrer UPDATE verwenden (damit Sie ON CONFLICT UPDATE für bestimmte Werte effektiv in ON CONFLICT IGNORE umwandeln können).

  • Die zum Einfügen vorgeschlagenen Werte sind als Zeilenvariable EXCLUDED verfügbar, die dieselbe Struktur wie die Zieltabelle hat. Sie können die ursprünglichen Werte in der Tabelle erhalten, indem Sie den Tabellennamen verwenden. In diesem Fall ist EXCLUDED.c10 (weil wir versucht haben, dies einzufügen), und "table".c ist 3, weil dies der aktuelle Wert in der Tabelle ist. Sie können einen oder beide der Ausdrücke SET und WHERE verwenden.

Hintergrundinformationen zu upsert finden Sie unter PSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) in PostgreSQL?

120
Craig Ringer

Ich suchte das Gleiche, als ich hierher kam, aber das Fehlen einer generischen "Upsert" -Funktion störte mich ein wenig, so dass ich dachte, Sie könnten einfach das Update übergeben und SQL als Argumente für diese Funktion aus dem Handbuch einfügen

das würde so aussehen:

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;
$$;

und vielleicht, um zu tun, was Sie anfangs wollten, stapeln Sie "Upsert", Sie könnten Tcl verwenden, um das sql_update zu teilen und die einzelnen Aktualisierungen zu schleifen, der Treffer für die Leistung wird sehr klein sein, siehe http: // archives. postgresql.org/pgsql-performance/2006-04/msg00557.php

die höchsten Kosten entstehen durch die Ausführung der Abfrage in Ihrem Code. Auf der Datenbankseite sind die Ausführungskosten viel geringer

17
Paul Scheltema

Es gibt keinen einfachen Befehl, um dies zu tun.

Der korrekteste Ansatz ist die Verwendung von Funktionen wie die von docs .

Eine andere (wenn auch nicht so sichere) Lösung besteht darin, ein Update mit Rückgabe durchzuführen, zu überprüfen, welche Zeilen aktualisiert wurden, und den Rest einzufügen

Etwas in der Art von:

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;

angenommen, ID: 2 wurde zurückgegeben:

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

Natürlich wird es früher oder später (in einer gleichzeitigen Umgebung) aussteigen, da es hier klare Rennbedingungen gibt, aber normalerweise wird es funktionieren.

Hier ist ein längerer und umfassenderer Artikel zum Thema .

13
user80168

Persönlich habe ich eine "Regel" eingerichtet, die an die Einfügeanweisung angehängt ist. Angenommen, Sie hatten eine "DNS" -Tabelle, die DNS-Treffer pro Kunde auf Zeitbasis aufzeichnete:

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

Sie wollten in der Lage sein, Zeilen mit aktualisierten Werten erneut einzufügen oder sie zu erstellen, wenn sie noch nicht vorhanden waren. Eingegeben auf die customer_id und die Uhrzeit. Etwas wie das:

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

Update: Dies kann möglicherweise fehlschlagen, wenn gleichzeitige Einfügungen erfolgen, da es zu unique_violation-Ausnahmen kommt. Die nicht abgeschlossene Transaktion wird jedoch fortgesetzt und ist erfolgreich, und Sie müssen die abgeschlossene Transaktion nur wiederholen.

Wenn jedoch ständig Unmengen von Einfügungen stattfinden, sollten Sie die Einfügungsanweisungen mit einer Tabellensperre versehen: SHARE ROW EXCLUSIVE-Sperren verhindern, dass Vorgänge Zeilen in Ihre Zieltabelle einfügen, löschen oder aktualisieren. Aktualisierungen, bei denen der eindeutige Schlüssel nicht aktualisiert wird, sind jedoch sicher. Wenn dies nicht möglich ist, verwenden Sie stattdessen Hinweissperren.

Der Befehl COPY verwendet auch keine REGELN. Wenn Sie also mit COPY einfügen, müssen Sie stattdessen Trigger verwenden.

9
Ch'marr

Ich benutzerdefinierte "Upsert" -Funktion oben, wenn Sie EINFÜGEN UND ERSETZEN möchten:

`

 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;`

Führen Sie nach der Ausführung Folgendes aus:

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

Es ist wichtig, ein Doppel-Dollar-Komma zu setzen, um Compiler-Fehler zu vermeiden

  • überprüfen Sie die Geschwindigkeit ...
8
Felipe FMMobile

Ähnlich wie die meistgelesene Antwort, funktioniert jedoch etwas schneller:

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)

(Quelle: http://www.the-art-of-web.com/sql/upsert/ )

7
alexkovelsky

Ich habe das gleiche Problem beim Verwalten von Kontoeinstellungen wie bei Name-Wert-Paaren. Das Designkriterium ist, dass verschiedene Clients unterschiedliche Einstellungssätze haben können.

Meine Lösung, ähnlich wie bei JWP, ist das Massenlöschen und -ersetzen, wodurch der Zusammenführungsdatensatz in Ihrer Anwendung generiert wird.

Dies ist ziemlich kugelsicher, plattformunabhängig und da es nie mehr als etwa 20 Einstellungen pro Client gibt, sind dies nur 3 DB-Aufrufe mit relativ geringer Last - wahrscheinlich die schnellste Methode.

Die Alternative, einzelne Zeilen zu aktualisieren - auf Ausnahmen zu prüfen und dann einzufügen - oder eine Kombination daraus ist abscheulicher Code, langsam und oft fehlerhaft, da (wie oben erwähnt) die SQL-Ausnahmebehandlung nicht dem Standard entspricht und von DB zu DB wechselt - oder sogar von Release zu Release.

 #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
6
benno

UPDATE gibt die Anzahl der geänderten Zeilen zurück. Wenn Sie JDBC (Java) verwenden, können Sie diesen Wert gegen 0 prüfen und stattdessen INSERT auslösen, wenn keine Zeilen betroffen sind. Wenn Sie eine andere Programmiersprache verwenden, kann die Anzahl der geänderten Zeilen möglicherweise noch ermittelt werden. Überprüfen Sie die Dokumentation.

Dies mag nicht so elegant sein, aber Sie haben viel einfacheres SQL, das im aufrufenden Code einfacher zu verwenden ist. Anders gesagt, wenn Sie das zehnzeilige Skript in PL/PSQL schreiben, sollten Sie wahrscheinlich einen Unit-Test der einen oder anderen Art nur für sich alleine haben.

5
h22

Zum Zusammenführen kleiner Mengen ist die Verwendung der obigen Funktion in Ordnung. Wenn Sie jedoch große Datenmengen zusammenführen, empfiehlt sich ein Blick auf http://mbk.projects.postgresql.org

Die derzeitige Best Practice, die mir bekannt ist, ist:

  1. KOPIERE neue/aktualisierte Daten in die temporäre Tabelle (sicher, oder du kannst INSERT machen, wenn die Kosten in Ordnung sind)
  2. Acquire Lock [optional] (Empfehlung ist Tabellensperren vorzuziehen, IMO)
  3. Verschmelzen. (der lustige Teil)
5
jwp

Gemäß der PostgreSQL-Dokumentation der INSERT-Anweisung wird die Behandlung des ON DUPLICATE KEY-Falls nicht unterstützt. Dieser Teil der Syntax ist eine proprietäre MySQL-Erweiterung.

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
Ahmad

Ich benutze diese Funktion zusammenführen

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
5
Mise

Edit: Das funktioniert nicht wie erwartet. Im Gegensatz zur akzeptierten Antwort führt dies zu eindeutigen Schlüsselverletzungen, wenn zwei Prozesse wiederholt upsert_foo gleichzeitig aufrufen.

Eureka! Ich habe in einer Abfrage eine Möglichkeit gefunden, dies zu tun: Verwenden Sie UPDATE ... RETURNING, um zu testen, ob Zeilen betroffen sind:

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;

Das UPDATE muss in einer separaten Prozedur durchgeführt werden, da dies leider ein Syntaxfehler ist:

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

Jetzt funktioniert es wie gewünscht:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');
4
Joey Adams