Die derzeit akzeptierte Antwort scheint für ein einzelnes Konfliktziel, wenige Konflikte, kleine Tupel und keine Auslöser in Ordnung zu sein. Es vermeidet Parallelitätsproblem 1 (siehe unten) mit Brute Force. Die einfache Lösung hat ihren Reiz, die Nebenwirkungen können weniger wichtig sein.
In allen anderen Fällen aktualisieren Sie identische Zeilen jedoch nicht ohne Notwendigkeit. Auch wenn Sie an der Oberfläche keinen Unterschied sehen, gibt es verschiedene Nebenwirkungen :
Es können Auslöser ausgelöst werden, die nicht ausgelöst werden sollten.
Es sperrt "unschuldige" Zeilen, was möglicherweise Kosten für gleichzeitige Transaktionen verursacht.
Die Zeile wird möglicherweise neu angezeigt, obwohl sie alt ist (Transaktionszeitstempel).
Am wichtigsten ist , dass mit dem MVCC-Modell von PostgreSQL für jeden eine neue Zeilenversion geschrieben wird UPDATE
, unabhängig davon, ob sich die Zeilendaten geändert haben. Dies führt zu einer Leistungsstrafe für das UPSERT selbst, zum Aufblähen der Tabelle, zum Aufblähen des Index, zur Leistungsstrafe für nachfolgende Operationen auf dem Tisch und zu den VACUUM
Kosten. Ein kleiner Effekt für wenige Duplikate, aber massiv für meistens Dupes.
Außerdem ist es manchmal nicht praktisch oder sogar möglich, es zu verwenden ON CONFLICT DO UPDATE
. Das Handbuch:
Für ON CONFLICT DO UPDATE
ein conflict_target
muss zur Verfügung gestellt werden.
Ein einzelnes "Konfliktziel" ist nicht möglich, wenn mehrere Indizes / Einschränkungen beteiligt sind.
Sie können (fast) dasselbe ohne leere Updates und Nebenwirkungen erreichen. Einige der folgenden Lösungen funktionieren auch mit ON CONFLICT DO NOTHING
(kein "Konfliktziel"), um alle möglichen Konflikte zu erfassen , die auftreten können - was wünschenswert sein kann oder nicht.
Ohne gleichzeitiges Schreiben
WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1') -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, ins AS (
INSERT INTO chats (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id --, usr, contact -- return more columns?
)
SELECT 'i' AS source -- 'i' for 'inserted'
, id --, usr, contact -- return more columns?
FROM ins
UNION ALL
SELECT 's' AS source -- 's' for 'selected'
, c.id --, usr, contact -- return more columns?
FROM input_rows
JOIN chats c USING (usr, contact); -- columns of unique index
Die source
Spalte ist eine optionale Ergänzung, um zu demonstrieren, wie dies funktioniert. Möglicherweise benötigen Sie es tatsächlich, um den Unterschied zwischen beiden Fällen zu erkennen (ein weiterer Vorteil gegenüber leeren Schreibvorgängen).
Das Finale JOIN chats
funktioniert, da neu eingefügte Zeilen aus einem angehängten datenmodifizierenden CTE in der zugrunde liegenden Tabelle noch nicht sichtbar sind. (Alle Teile derselben SQL-Anweisung sehen dieselben Snapshots der zugrunde liegenden Tabellen.)
Da der VALUES
Ausdruck freistehend ist (nicht direkt an einen angehängt INSERT
), kann Postgres keine Datentypen aus den Zielspalten ableiten, und Sie müssen möglicherweise explizite Typumwandlungen hinzufügen. Das Handbuch:
Bei VALUES
Verwendung in INSERT
werden alle Werte automatisch zum Datentyp der entsprechenden Zielspalte gezwungen. Wenn es in anderen Kontexten verwendet wird, muss möglicherweise der richtige Datentyp angegeben werden. Wenn die Einträge alle Literalkonstanten in Anführungszeichen sind, reicht es aus, die erste zu erzwingen, um den angenommenen Typ für alle zu bestimmen.
Die Abfrage selbst (ohne Berücksichtigung der Nebenwirkungen) kann für einige Dupes aufgrund des Overheads des CTE und der zusätzlichen Abfrage etwas teurer sein SELECT
(was billig sein sollte, da der perfekte Index per Definition vorhanden ist - eine eindeutige Einschränkung wird mit implementiert ein Index).
Kann für viele Duplikate (viel) schneller sein . Die effektiven Kosten für zusätzliche Schreibvorgänge hängen von vielen Faktoren ab.
Aber es gibt auf jeden Fall weniger Nebenwirkungen und versteckte Kosten . Es ist höchstwahrscheinlich insgesamt billiger.
Angehängte Sequenzen sind noch weit fortgeschritten, da vor dem Testen auf Konflikte Standardwerte eingegeben werden .
Über CTEs:
Bei gleichzeitiger Schreiblast
Standardtransaktionsisolation READ COMMITTED
annehmen . Verbunden:
Die beste Strategie zur Abwehr von Rennbedingungen hängt von den genauen Anforderungen, der Anzahl und Größe der Zeilen in der Tabelle und in den UPSERTs, der Anzahl der gleichzeitigen Transaktionen, der Wahrscheinlichkeit von Konflikten, den verfügbaren Ressourcen und anderen Faktoren ab.
Parallelitätsproblem 1
Wenn eine gleichzeitige Transaktion in eine Zeile geschrieben wurde, die Ihre Transaktion jetzt zu UPSERT versucht, muss Ihre Transaktion warten, bis die andere abgeschlossen ist.
Wenn die andere Transaktion mit ROLLBACK
(oder einem Fehler, dh automatisch ROLLBACK
) endet , kann Ihre Transaktion normal fortgesetzt werden. Geringfügige mögliche Nebenwirkung: Lücken in fortlaufenden Nummern. Aber keine fehlenden Zeilen.
Wenn die andere Transaktion normal endet (implizit oder explizit COMMIT
), INSERT
erkennen Sie einen Konflikt (der UNIQUE
Index / die Einschränkung ist absolut) und geben DO NOTHING
daher auch die Zeile nicht zurück. (Kann die Zeile auch nicht sperren, wie in Problem 2 der Parallelität unten gezeigt, da sie nicht sichtbar ist .) Der SELECT
sieht denselben Snapshot vom Beginn der Abfrage an und kann auch die noch unsichtbare Zeile nicht zurückgeben.
Solche Zeilen fehlen in der Ergebnismenge (obwohl sie in der zugrunde liegenden Tabelle vorhanden sind)!
Dies kann in Ordnung sein, wie es ist . Insbesondere, wenn Sie keine Zeilen wie im Beispiel zurückgeben und zufrieden sind, dass die Zeile vorhanden ist. Wenn das nicht gut genug ist, gibt es verschiedene Möglichkeiten, dies zu umgehen.
Sie können die Zeilenanzahl der Ausgabe überprüfen und die Anweisung wiederholen, wenn sie nicht mit der Zeilenanzahl der Eingabe übereinstimmt. Kann für den seltenen Fall gut genug sein. Der Punkt ist, eine neue Abfrage zu starten (kann sich in derselben Transaktion befinden), in der dann die neu festgeschriebenen Zeilen angezeigt werden.
Oder überprüfen Ergebniszeilen für fehlende innerhalb derselben Abfrage und überschreibt die mit dem Brute - Force - Trick in demonstriert Alextoni Antwort .
WITH input_rows(usr, contact, name) AS ( ... ) -- see above
, ins AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id, usr, contact -- we need unique columns for later join
)
, sel AS (
SELECT 'i'::"char" AS source -- 'i' for 'inserted'
, id, usr, contact
FROM ins
UNION ALL
SELECT 's'::"char" AS source -- 's' for 'selected'
, c.id, usr, contact
FROM input_rows
JOIN chats c USING (usr, contact)
)
, ups AS ( -- RARE corner case
INSERT INTO chats AS c (usr, contact, name) -- another UPSERT, not just UPDATE
SELECT i.*
FROM input_rows i
LEFT JOIN sel s USING (usr, contact) -- columns of unique index
WHERE s.usr IS NULL -- missing!
ON CONFLICT (usr, contact) DO UPDATE -- we've asked nicely the 1st time ...
SET name = c.name -- ... this time we overwrite with old value
-- SET name = EXCLUDED.name -- alternatively overwrite with *new* value
RETURNING 'u'::"char" AS source -- 'u' for updated
, id --, usr, contact -- return more columns?
)
SELECT source, id FROM sel
UNION ALL
TABLE ups;
Es ist wie bei der obigen Abfrage, aber wir fügen dem CTE noch einen Schritt hinzu ups
, bevor wir die vollständige Ergebnismenge zurückgeben. Dieser letzte CTE wird die meiste Zeit nichts tun. Nur wenn Zeilen im zurückgegebenen Ergebnis verloren gehen, wenden wir Brute Force an.
Noch mehr Aufwand. Je mehr Konflikte mit bereits vorhandenen Zeilen auftreten, desto wahrscheinlicher ist es, dass dies den einfachen Ansatz übertrifft.
Ein Nebeneffekt: Das 2. UPSERT schreibt Zeilen in unregelmäßiger Reihenfolge, sodass die Möglichkeit von Deadlocks (siehe unten) wieder eingeführt wird, wenn sich drei oder mehr Transaktionen, die in dieselben Zeilen schreiben, überlappen. Wenn das ein Problem ist, brauchen Sie eine andere Lösung - wie das Wiederholen der gesamten Aussage wie oben erwähnt.
Parallelitätsproblem 2
Wenn gleichzeitige Transaktionen in betroffene Spalten betroffener Zeilen schreiben können und Sie sicherstellen müssen, dass die gefundenen Zeilen zu einem späteren Zeitpunkt in derselben Transaktion noch vorhanden sind, können Sie vorhandene Zeilen im CTE kostengünstig sperrenins
(die sonst entsperrt würden). mit:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
Und fügen Sie ein in die Verriegelungs Klausel SELECT
als auch, wieFOR UPDATE
.
Dadurch warten konkurrierende Schreibvorgänge bis zum Ende der Transaktion, wenn alle Sperren aufgehoben sind. Also sei kurz.
Weitere Details und Erklärungen:
Deadlocks?
Verteidigen Sie sich gegen Deadlocks, indem Sie Zeilen in konsistenter Reihenfolge einfügen . Sehen:
Datentypen und Casts
Vorhandene Tabelle als Vorlage für Datentypen ...
Explizite Typumwandlungen für die erste Datenzeile im freistehenden VALUES
Ausdruck können unpraktisch sein. Es gibt Möglichkeiten, dies zu umgehen. Sie können jede vorhandene Beziehung (Tabelle, Ansicht, ...) als Zeilenvorlage verwenden. Die Zieltabelle ist die offensichtliche Wahl für den Anwendungsfall. Eingabedaten werden automatisch zu geeigneten Typen gezwungen, wie in der VALUES
Klausel eines INSERT
:
WITH input_rows AS (
(SELECT usr, contact, name FROM chats LIMIT 0) -- only copies column names and types
UNION ALL
VALUES
('foo1', 'bar1', 'bob1') -- no type casts here
, ('foo2', 'bar2', 'bob2')
)
...
Dies funktioniert bei einigen Datentypen nicht. Sehen:
... und Namen
Dies funktioniert auch für alle Datentypen.
Beim Einfügen in alle (führenden) Spalten der Tabelle können Sie Spaltennamen weglassen. Angenommen, die Tabelle chats
im Beispiel besteht nur aus den 3 im UPSERT verwendeten Spalten:
WITH input_rows AS (
SELECT * FROM (
VALUES
((NULL::chats).*) -- copies whole row definition
('foo1', 'bar1', 'bob1') -- no type casts needed
, ('foo2', 'bar2', 'bob2')
) sub
OFFSET 1
)
...
Nebenbei: Verwenden Sie keine reservierten Wörter wie "user"
als Bezeichner. Das ist eine geladene Fußwaffe. Verwenden Sie legale, nicht zitierte Bezeichner in Kleinbuchstaben. Ich habe es durch ersetzt usr
.
ON CONFLICT UPDATE
damit die Zeile geändert wird. DannRETURNING
wird es erfasst.