Bei doppeltem Update in PostgreSQL einfügen?

641

Vor einigen Monaten habe ich aus einer Antwort auf Stack Overflow gelernt, wie mehrere Updates gleichzeitig in MySQL mit der folgenden Syntax ausgeführt werden:

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 ist dies nicht korrekt. Es bezieht sich auf alle korrekten Tabellen, daher gehe ich davon aus, dass verschiedene Schlüsselwörter verwendet werden, aber ich bin mir nicht sicher, wo dies in der PostgreSQL-Dokumentation behandelt wird.

Zur Verdeutlichung möchte ich einige Dinge einfügen und wenn sie bereits vorhanden sind, um sie zu aktualisieren.

Teifion
quelle
38
Jeder, der diese Frage findet, sollte Depesz 'Artikel "Warum ist Upsert so kompliziert?" Lesen. . Es erklärt das Problem und mögliche Lösungen sehr gut.
Craig Ringer
8
UPSERT wird in Postgres 9.5 hinzugefügt: wiki.postgresql.org/wiki/…
am
4
@tommed - es getan wurde: stackoverflow.com/a/34639631/4418
warren

Antworten:

512

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

Wenn Sie in den E-Mail-Gruppenarchiven von postgresql nach "upsert" suchen, finden Sie im Handbuch ein Beispiel dafür, was Sie möglicherweise tun möchten :

Beispiel 38-2. Ausnahmen mit UPDATE / INSERT

In diesem Beispiel wird die Ausnahmebehandlung verwendet, um je nach Bedarf 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');

Möglicherweise gibt es in der Hacker-Mailingliste ein Beispiel dafür, wie dies in großen Mengen mithilfe von CTEs in Version 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 a_horse_with_no_name Antwort für ein klareres Beispiel.

Stephen Denne
quelle
7
Das einzige, was mir daran nicht gefällt, ist, dass es viel langsamer wäre, weil jeder Upsert sein eigener Aufruf in die Datenbank wäre.
baash05
@ baash05 Möglicherweise gibt es eine Möglichkeit, dies in großen Mengen zu tun. Siehe meine aktualisierte Antwort.
Stephen Denne
2
Das einzige, was ich anders machen würde, ist, FOR 1..2 LOOP anstelle von nur LOOP zu verwenden, damit sich eine andere eindeutige Einschränkung nicht auf unbestimmte Zeit dreht, wenn sie verletzt wird.
Olamork
2
Worauf bezieht excludedsich das in der ersten Lösung hier?
Ichbinallen
2
@ichbinallen in den Dokumenten Die Klauseln SET und WHERE in ON CONFLICT DO UPDATE haben Zugriff auf die vorhandene Zeile unter Verwendung des Tabellennamens (oder eines Alias) und auf Zeilen, die zum Einfügen unter Verwendung der speziellen ausgeschlossenen Tabelle vorgeschlagen wurden . In diesem Fall erhalten Sie über die spezielle excludedTabelle Zugriff auf die Werte, die Sie ursprünglich einfügen wollten.
TMichel
429

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


Eine andere clevere Möglichkeit, ein "UPSERT" in postgresql zu erstellen, 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 zu 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 Rennbedingungen, die bedeuten, dass es bei gleichzeitiger Einfügung einer Zeile immer noch mit einem doppelten Schlüsselfehler fehlschlägt oder beim gleichzeitigen Löschen einer Zeile ohne eingefügte Zeile endet . Eine SERIALIZABLETransaktion unter PostgreSQL 9.1 oder höher wird auf Kosten einer sehr hohen Serialisierungsfehlerrate zuverlässig abgewickelt, was bedeutet, dass Sie viel wiederholen müssen. Sehen Sie, warum Upsert so kompliziert ist , und diskutieren Sie diesen Fall ausführlicher.

Dieser Ansatz unterliegt auch isoliert verlorenen Aktualisierungen, es read committedsei denn, die Anwendung überprüft die Anzahl der betroffenen Zeilen und überprüft, ob entweder die insertoder die updatebetroffene Zeile vorhanden ist .

Rinder-
quelle
6
Kurze Antwort: Wenn der Datensatz vorhanden ist, führt INSERT nichts aus. Lange Antwort: Das SELECT im INSERT gibt so viele Ergebnisse zurück, wie Übereinstimmungen mit der where-Klausel vorliegen. Das ist höchstens eins (wenn die Nummer eins nicht im Ergebnis der Unterauswahl enthalten ist), andernfalls null. Das INSERT fügt somit entweder eine oder null Zeilen hinzu.
Peter Becker
3
Der ' ... where not exists (select 1 from table where id = 3);
Wo'
1
Dies sollte die richtige Antwort sein. Mit ein paar kleinen Änderungen könnte es verwendet werden, um ein
Massenupdate durchzuführen
1
@keaplogik, diese 9.1-Einschränkung gilt für beschreibbaren CTE (Common Table Expressions), der in einer anderen Antwort beschrieben wird. Die in dieser Antwort verwendete Syntax ist sehr einfach und wird seit langem unterstützt.
Rinder
8
Achtung, dieses Thema , um verlorenes Updates in read committedIsolation , es sei denn Ihre Anwendung überprüft , um sicherzustellen , dass der insertoder die updateeine von Null verschiedenen rowcount hat. Siehe dba.stackexchange.com/q/78510/7788
Craig Ringer
227

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 eindeutige Schlüsselverletzung nicht verhindert, jedoch nicht für verlorene Updates anfällig ist.
Siehe das Follow-up von Craig Ringer auf dba.stackexchange.com

ein Pferd ohne Name
quelle
1
@ FrançoisBeausoleil: Die Wahrscheinlichkeit einer Rennbedingung ist viel geringer als beim "Try / Handle Exception" -Ansatz
a_horse_with_no_name
2
@a_horse_with_no_name Wie meinst du genau, dass die Chance auf Rennbedingungen viel geringer ist? Wenn ich diese Abfrage gleichzeitig mit denselben Datensätzen ausführe, wird in 100% der Fälle der Fehler "Doppelter Schlüsselwert verletzt eindeutige Einschränkung" angezeigt, bis die Abfrage feststellt, dass der Datensatz eingefügt wurde. Ist das ein vollständiges Beispiel?
Jeroen van Dijk
4
@a_horse_with_no_name Ihre Lösung scheint in gleichzeitigen Situationen zu funktionieren, wenn Sie die Upsert-Anweisung mit der folgenden Sperre umschließen: BEGIN WORK; LOCK TABLE mytable IM EXKLUSIVEN SHARE ROW-MODUS; <UPSERT HIER>; COMMIT WORK;
Jeroen van Dijk
2
@JeroenvanDijk: Danke. Was ich mit "viel kleiner" gemeint habe, ist, dass bei mehreren Transaktionen (und dem Festschreiben der Änderung!) Die Zeitspanne zwischen dem Update und dem Einfügen kleiner ist, da alles nur eine einzige Anweisung ist. Sie können jederzeit eine pk-Verletzung durch zwei unabhängige INSERT-Anweisungen generieren. Wenn Sie die gesamte Tabelle sperren, serialisieren Sie effektiv den gesamten Zugriff darauf (was Sie auch mit der serialisierbaren Isolationsstufe erreichen können).
a_horse_with_no_name
12
Diese Lösung kann verloren gehen, wenn die Einfügetransaktion zurückgesetzt wird. Es gibt keine Überprüfung, um zu erzwingen, dass die UPDATEZeilen betroffen sind.
Craig Ringer
131

In PostgreSQL 9.5 und höher können Sie verwenden INSERT ... ON CONFLICT UPDATE.

Siehe die Dokumentation .

Ein MySQL INSERT ... ON DUPLICATE KEY UPDATEkann direkt in a umformuliert werden ON CONFLICT UPDATE. SQL-Standard-Syntax ist ebenfalls nicht vorhanden, beide sind datenbankspezifische Erweiterungen. Es gibt gute Gründe, warum dies MERGEnicht 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.)

zB gegebenes 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 den eindeutigen Einschränkungsnamen) angeben, der für die Eindeutigkeitsprüfung verwendet werden soll. Das ist dasON CONFLICT (columnname) DO

  • Das Schlüsselwort SETmuss so verwendet werden, als wäre dies eine normale UPDATEAnweisung

Es hat auch einige nette Funktionen:

  • Sie können eine WHEREKlausel auf Ihrem haben UPDATE(damit Sie sich effektiv ON CONFLICT UPDATEin ON CONFLICT IGNOREbestimmte Werte verwandeln können)

  • Die zum Einfügen vorgeschlagenen Werte sind als Zeilenvariable verfügbar EXCLUDED, die dieselbe Struktur wie die Zieltabelle hat. Sie können die ursprünglichen Werte in der Tabelle mithilfe des Tabellennamens abrufen. Also in diesem Fall EXCLUDED.csein wird 10(denn das ist , was wir versuchen , einfügen) und "table".cwird , 3da , dass der aktuelle Wert in der Tabelle ist. Sie können einen oder beide in den SETAusdrücken und der WHEREKlausel verwenden.

Hintergrundinformationen zu Upsert finden Sie unter UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) in PostgreSQL.

Craig Ringer
quelle
Ich habe mir die 9.5-Lösung von PostgreSQL angesehen, wie Sie oben beschrieben haben, weil ich unter MySQL Lücken im Feld für die automatische Inkrementierung hatte ON DUPLICATE KEY UPDATE. Ich habe Postgres 9.5 heruntergeladen und Ihren Code implementiert, aber seltsamerweise tritt unter Postgres das gleiche Problem auf: Das serielle Feld des Primärschlüssels ist nicht fortlaufend (es gibt Lücken zwischen den Einfügungen und Aktualisierungen). Irgendeine Idee, was hier los ist? Ist das normal? Irgendeine Idee, wie man dieses Verhalten vermeidet? Vielen Dank.
WM
@WM Das ist so ziemlich eine Upsert-Operation. Sie müssen die Funktion, die die Sequenz generiert, auswerten, bevor Sie das Einfügen versuchen. Da solche Sequenzen für den gleichzeitigen Betrieb ausgelegt sind, sind sie von der normalen Transaktionssemantik ausgenommen. Selbst wenn dies nicht der Fall ist, wird die Generierung nicht in einer Subtransaktion aufgerufen und zurückgesetzt. Sie wird normal abgeschlossen und mit dem Rest des Vorgangs festgeschrieben. Dies würde also selbst bei "lückenlosen" Sequenzimplementierungen passieren. Die einzige Möglichkeit, die DB zu vermeiden, besteht darin, die Auswertung der Sequenzgenerierung bis nach der Schlüsselprüfung zu verzögern.
Craig Ringer
1
@WM, die ihre eigenen Probleme schaffen würde. Grundsätzlich steckst du fest. Wenn Sie sich jedoch darauf verlassen, dass serial / auto_increment lückenlos ist, haben Sie bereits Fehler. Sie können Sequenzlücken aufgrund Rollbacks einschließlich transiente Fehler haben - Neustarts unter Last, Client - Fehler Mitte Transaktion, Abstürze, etc. Sie müssen nie, nie verlassen auf SERIAL/ SEQUENCEoder AUTO_INCREMENTkeine Lücken aufweisen. Wenn Sie lückenlose Sequenzen benötigen, sind diese komplexer. Normalerweise müssen Sie einen Zählertisch verwenden. Google wird Ihnen mehr erzählen. Beachten Sie jedoch, dass lückenlose Sequenzen alle Einfügungen gleichzeitig verhindern.
Craig Ringer
@WM Wenn Sie unbedingt lückenlose Sequenzen und Upsert benötigen, können Sie den im Handbuch beschriebenen funktionsbasierten Upsert-Ansatz zusammen mit einer lückenlosen Sequenzimplementierung verwenden, die eine Zählertabelle verwendet. Da die BEGIN ... EXCEPTION ...Ausführung in einer Subtransaktion erfolgt, die bei einem Fehler zurückgesetzt wird, wird Ihr Sequenzinkrement zurückgesetzt, wenn dies INSERTfehlschlägt.
Craig Ringer
Vielen Dank @Craig Ringer, das war ziemlich informativ. Mir wurde klar, dass ich einfach aufgeben kann, diesen Primärschlüssel mit automatischer Inkrementierung zu haben. Ich habe eine zusammengesetzte Primärdatenbank aus 3 Feldern erstellt und für meinen speziellen aktuellen Bedarf ist wirklich kein lückenloses Auto-Inkrement-Feld erforderlich. Nochmals vielen Dank, die von Ihnen angegebenen Informationen würden mir in Zukunft Zeit sparen, um ein natürliches und gesundes DB-Verhalten zu verhindern. Ich verstehe es jetzt besser.
WM
17

Ich habe nach dem gleichen gesucht, als ich hierher kam, aber das Fehlen einer generischen "Upsert" -Funktion hat mich ein wenig gestört, 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 das zu tun, was Sie ursprünglich tun wollten, Batch "Upsert", könnten Sie Tcl verwenden, um das sql_update zu teilen und die einzelnen Updates zu schleifen. Der Leistungstreffer wird sehr gering sein, siehe http://archives.postgresql.org/pgsql- Leistung / 2006-04 / msg00557.php

Die höchsten Kosten sind die Ausführung der Abfrage aus Ihrem Code. Auf der Datenbankseite sind die Ausführungskosten viel geringer

Paul Scheltema
quelle
3
Sie müssen dies immer noch in einer Wiederholungsschleife ausführen und es ist anfällig für Rennen mit gleichzeitiger Ausführung, es DELETEsei denn, Sie sperren die Tabelle oder befinden sich in SERIALIZABLEPostgreSQL 9.1 oder höher in Transaktionsisolation.
Craig Ringer
13

Es gibt keinen einfachen Befehl, dies zu tun.

Der korrekteste Ansatz besteht darin, Funktionen wie die aus Dokumenten zu verwenden .

Eine andere Lösung (obwohl nicht so sicher) 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 hier klare Rennbedingungen herrschen, aber normalerweise wird es funktionieren.

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

Craig Ringer
quelle
1
Wenn Sie diese Option verwenden, stellen Sie sicher, dass die ID zurückgegeben wird, auch wenn das Update nichts bewirkt. Ich habe Datenbanken gesehen, die Abfragen wie "Tabelle foo set bar = 4, wobei bar = 4" optimieren.
Thelem
10

Persönlich habe ich eine "Regel" eingerichtet, die der Einfügeanweisung beigefügt ist. Angenommen, Sie hatten eine "DNS" -Tabelle, in der DNS-Treffer pro Kunde pro Zeit aufgezeichnet wurden:

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. Geben Sie die customer_id und die Uhrzeit ein. 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 fehlschlagen, wenn gleichzeitig Einfügungen durchgeführt werden, da dies zu eindeutigen Ausnahmen für Gewalt führt. Die nicht abgebrochene Transaktion wird jedoch fortgesetzt und ist erfolgreich, und Sie müssen nur die abgebrochene Transaktion wiederholen.

Wenn jedoch ständig Unmengen von Einfügungen auftreten, sollten Sie die Einfügeanweisungen mit einer Tabellensperre versehen: Durch die Sperrung von SHARE ROW EXCLUSIVE werden Vorgänge verhindert, durch die Zeilen in Ihre Zieltabelle eingefügt, gelöscht oder aktualisiert werden können. Aktualisierungen, bei denen der eindeutige Schlüssel nicht aktualisiert wird, sind jedoch sicher. Wenn Sie dies nicht tun, verwenden Sie stattdessen Hinweisschlösser.

Außerdem verwendet der Befehl COPY keine REGELN. Wenn Sie also mit COPY einfügen, müssen Sie stattdessen Trigger verwenden.

Ch'marr
quelle
9

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
Mise
quelle
1
Es ist effizienter, einfach die updateerste Zeile auszuführen und dann die Anzahl der aktualisierten Zeilen zu überprüfen. (Siehe Ahmads Antwort)
a_horse_with_no_name
8

Ich habe oben die Funktion "Upsert" angepasst, 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;`

Und nachdem Sie ausgeführt haben, machen Sie so etwas:

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

Es ist wichtig, ein doppeltes Dollar-Komma zu setzen, um Compilerfehler zu vermeiden

  • Überprüfen Sie die Geschwindigkeit ...
Felipe FMMobile
quelle
7

Ähnlich wie die beliebteste 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/ )

alexkovelsky
quelle
3
Dies schlägt fehl, wenn es in zwei Sitzungen gleichzeitig ausgeführt wird, da bei keinem Update eine vorhandene Zeile angezeigt wird, sodass beide Updates keine Zeilen mehr enthalten und beide Abfragen eine Einfügung ausgeben.
Craig Ringer
6

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

Meine Lösung, ähnlich wie bei JWP, besteht darin, sie in großen Mengen zu löschen und zu ersetzen und den Zusammenführungsdatensatz in Ihrer Anwendung zu generieren.

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

Die Alternative, einzelne Zeilen zu aktualisieren - nach Ausnahmen zu suchen und dann einzufügen - oder eine Kombination davon ist abscheulicher Code, der langsam ist und häufig unterbrochen wird, weil (wie oben erwähnt) die nicht standardmäßige Behandlung von SQL-Ausnahmen von Datenbank zu Datenbank wechselt - oder sogar von Version zu Version.

 #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
benno
quelle
Willkommen bei SO. Schöne Einführung! :-)
Don Frage
1
Dies ist eher so REPLACE INTOals INSERT INTO ... ON DUPLICATE KEY UPDATE, was ein Problem verursachen kann, wenn Sie Trigger verwenden. Am Ende werden Sie Trigger / Regeln löschen und einfügen, anstatt diese zu aktualisieren.
CHao
5

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

Christian Hang-Hicks
quelle
@ Lucian MERGEist auch eher eine OLAP-Operation; Erläuterungen finden Sie unter stackoverflow.com/q/17267417/398670 . Es definiert keine Parallelitätssemantik und die meisten Leute, die es für Upsert verwenden, erstellen nur Fehler.
Craig Ringer
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
Ahmad
quelle
5

Zum Zusammenführen kleiner Mengen ist die Verwendung der obigen Funktion in Ordnung. Wenn Sie jedoch große Datenmengen zusammenführen, empfehlen wir Ihnen, http://mbk.projects.postgresql.org zu besuchen

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

  1. KOPIEREN Sie neue / aktualisierte Daten in die temporäre Tabelle (sicher, oder Sie können INSERT ausführen, wenn die Kosten in Ordnung sind).
  2. Acquire Lock [optional] (Empfehlung ist Tischschlössern vorzuziehen, IMO)
  3. Verschmelzen. (der lustige Teil)
jwp
quelle
5

UPDATE gibt die Anzahl der geänderten Zeilen zurück. Wenn Sie JDBC (Java) verwenden, können Sie diesen Wert mit 0 vergleichen 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 abgerufen werden. Überprüfen Sie die Dokumentation.

Dies ist möglicherweise nicht so elegant, aber Sie haben viel einfacheres SQL, das aus dem aufrufenden Code trivialer zu verwenden ist. Wenn Sie das zehnzeilige Skript in PL / PSQL schreiben, sollten Sie wahrscheinlich einen Unit-Test der einen oder anderen Art nur dafür durchführen.

Audrius Meskauskas
quelle
4

Bearbeiten: Dies funktioniert nicht wie erwartet. Im Gegensatz zur akzeptierten Antwort führt dies zu eindeutigen Schlüsselverletzungen, wenn zwei Prozesse wiederholt upsert_foogleichzeitig aufgerufen werden .

Eureka! Ich habe in einer Abfrage einen Weg gefunden, dies zu tun: Verwenden Sie diese Option, um UPDATE ... RETURNINGzu 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 UPDATEmuss in einem gesonderten Verfahren durchgeführt werden , denn leider ist dies 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');
Joey Adams
quelle
1
Sie können sie zu einer Anweisung kombinieren, wenn Sie einen beschreibbaren CTE verwenden. Aber wie die meisten hier veröffentlichten Lösungen ist diese falsch und schlägt bei gleichzeitigen Updates fehl.
Craig Ringer