PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Wie verwende ich RETURNING mit ON CONFLICT in PostgreSQL?

Die derzeit akzeptierte Antwort scheint für ein einzelnes Konfliktziel, wenige Konflikte, kleine Tupel und keine Auslöser in Ordnung zu sein. Es vermeidet Gleichzeitigkeitsproblem 1 (siehe unten) mit roher Gewalt. Die einfache Lösung hat ihren Reiz, die Nebenwirkungen mögen weniger ins Gewicht fallen.

In allen anderen Fällen jedoch nicht Aktualisieren Sie identische Zeilen ohne Notwendigkeit. Auch wenn Sie an der Oberfläche keinen Unterschied sehen, gibt es verschiedene Nebenwirkungen :

  • Es könnte Auslöser auslösen, die nicht ausgelöst werden sollten.

  • Es sperrt "unschuldige" Zeilen, wodurch möglicherweise Kosten für gleichzeitige Transaktionen entstehen.

  • Dadurch könnte die Zeile neu erscheinen, obwohl sie alt ist (Zeitstempel der Transaktion).

  • Am wichtigsten , mit dem MVCC-Modell von PostgreSQL wird für jedes UPDATE eine neue Zeilenversion geschrieben , egal ob sich die Zeilendaten geändert haben. Dies führt zu einer Leistungseinbuße für den UPSERT selbst, Tabellenaufblähung, Indexaufblähung, Leistungseinbuße für nachfolgende Operationen auf der Tabelle, VACUUM kosten. Ein kleiner Effekt für wenige Duplikate, aber massiv für meistens Dupes.

Plus , manchmal ist es nicht praktikabel oder sogar möglich, ON CONFLICT DO UPDATE zu verwenden . Das Handbuch:

Für ON CONFLICT DO UPDATE , ein conflict_target muss angegeben werden.

Eine einzelne "Konfliktziel" ist nicht möglich, wenn mehrere Indizes / Constraints beteiligt sind. Aber hier ist eine verwandte Lösung für mehrere Teilindizes:

  • UPSERT basierend auf UNIQUE-Einschränkung mit NULL-Werten

Zurück zum Thema, 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 zu fangen mögliche Konflikte, die auftreten könnten - die wünschenswert oder nicht wünschenswert sein können.

Ohne gleichzeitige Schreiblast

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

Die letzten JOIN chats funktioniert, da neu eingefügte Zeilen aus einem angehängten datenmodifizierenden CTE noch nicht in der zugrunde liegenden Tabelle sichtbar sind. (Alle Teile derselben SQL-Anweisung sehen dieselben Snapshots der zugrunde liegenden Tabellen.)

Da die VALUES Ausdruck ist freistehend (nicht direkt an INSERT angehängt ) Postgres kann keine Datentypen aus den Zielspalten ableiten und Sie müssen möglicherweise explizite Typumwandlungen hinzufügen. Das Handbuch:

Wenn VALUES wird in INSERT verwendet , werden alle Werte automatisch in den Datentyp der entsprechenden Zielspalte umgewandelt. Bei Verwendung in anderen Kontexten kann es erforderlich sein, den richtigen Datentyp anzugeben. Wenn die Einträge alle in Anführungszeichen gesetzte Literalkonstanten sind, reicht es aus, die erste zu erzwingen, um den angenommenen Typ für alle zu bestimmen.

Die Abfrage selbst (die Nebenwirkungen nicht mitgezählt) kann für wenige etwas teurer sein Duplikate, aufgrund des Overheads des CTE und des zusätzlichen SELECT (was billig sein sollte, da der perfekte Index per Definition vorhanden ist - eine Eindeutigkeitsbeschränkung wird mit einem Index implementiert).

Kann für viele (viel) schneller sein Duplikate. Die effektiven Kosten für zusätzliche Schreibvorgänge hängen von vielen Faktoren ab.

Aber es gibt weniger Nebenwirkungen und versteckte Kosten auf jeden Fall. Es ist höchstwahrscheinlich insgesamt billiger.

Angehängte Sequenzen werden immer noch erweitert, da Standardwerte vorher ausgefüllt werden Testen auf Konflikte.

Über CTEs:

  • Sind Abfragen vom Typ SELECT der einzige Typ, der verschachtelt werden kann?
  • Deduplizierte SELECT-Anweisungen in relationaler Division

Mit gleichzeitiger Schreiblast

Angenommen, standardmäßig READ COMMITTED Transaktionsisolierung. Verwandte:

  • Gleichzeitige Transaktionen führen zu einer Race-Condition mit eindeutiger Einschränkung beim Einfügen

Die beste Strategie zur Abwehr von Race Conditions hängt von den genauen Anforderungen, der Anzahl und Größe der Zeilen in der Tabelle und in den UPSERTs, der Anzahl gleichzeitiger Transaktionen, der Wahrscheinlichkeit von Konflikten, verfügbaren Ressourcen und anderen Faktoren ab ...

Gleichzeitigkeitsproblem 1

Wenn eine gleichzeitige Transaktion in eine Zeile geschrieben hat, die Ihre Transaktion nun zu UPSERT versucht, muss Ihre Transaktion warten, bis die andere abgeschlossen ist.

Wenn die andere Transaktion mit ROLLBACK endet (oder irgendein Fehler, d.h. automatisches ROLLBACK ), kann Ihre Transaktion normal fortgesetzt werden. Geringfügiger möglicher Nebeneffekt:Lücken in fortlaufenden Nummern. Aber keine fehlenden Zeilen.

Wenn die andere Transaktion normal endet (implizites oder explizites COMMIT ), Ihr INSERT erkennt einen Konflikt (der UNIQUE Index / Constraint ist absolut) und DO NOTHING , daher auch nicht die Zeile zurückgeben. (Die Zeile kann auch nicht gesperrt werden, wie in Gleichzeitigkeitsproblem 2 gezeigt unten, da es nicht sichtbar ist .) Das SELECT sieht denselben Snapshot vom Beginn der Abfrage und kann auch die noch unsichtbare Zeile nicht zurückgeben.

Alle diese Zeilen fehlen in der Ergebnismenge (obwohl sie in der zugrunde liegenden Tabelle vorhanden sind)!

Dies kann so in Ordnung sein . Vor allem, wenn Sie keine Zeilen wie im Beispiel zurückgeben und zufrieden sind, wenn Sie wissen, dass die Zeile vorhanden ist. Wenn das nicht gut genug ist, gibt es verschiedene Möglichkeiten, es 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), die dann die neu festgeschriebenen Zeilen sieht.

Oder Suche nach fehlenden Ergebniszeilen innerhalb die gleiche Abfrage und überschreiben diejenigen mit dem Brute-Force-Trick, der in Alextonis Antwort gezeigt wird.

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 die Abfrage oben, aber wir fügen mit dem CTE ups einen weiteren Schritt hinzu , bevor wir das vollständige zurückgeben Ergebnissatz. Dieser letzte CTE wird die meiste Zeit nichts tun. Nur wenn im zurückgegebenen Ergebnis Zeilen fehlen, wenden wir Brute Force an.

Noch mehr Overhead. Je mehr Konflikte mit bereits vorhandenen Zeilen auftreten, desto wahrscheinlicher übertrifft dies den einfachen Ansatz.

Ein Nebeneffekt:Der 2. UPSERT schreibt Zeilen in falscher Reihenfolge, sodass die Möglichkeit von Deadlocks (siehe unten) wieder eingeführt wird, wenn drei oder mehr Transaktionen, die in dieselben Zeilen schreiben, überschneiden sich. Wenn das ein Problem ist, brauchen Sie eine andere Lösung - wie das Wiederholen der gesamten Anweisung wie oben erwähnt.

Gleichzeitigkeitsproblem 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 sperren billig in den CTE ins (die sonst entsperrt werden 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 dem SELECT eine Sperrklausel hinzu ebenso wie FOR UPDATE .

Dadurch werden konkurrierende Schreiboperationen bis zum Ende der Transaktion gewartet, wenn alle Sperren freigegeben werden. Fassen Sie sich also kurz.

Weitere Details und Erläuterungen:

  • So schließen Sie ausgeschlossene Zeilen in RETURNING von INSERT ... ON CONFLICT ein
  • Ist SELECT oder INSERT in einer Funktion anfällig für Race-Conditions?

Deadlocks?

Schützen Sie sich vor Deadlocks durch Einfügen von Zeilen in konsistenter Reihenfolge . Siehe:

  • Deadlock bei mehrzeiligen INSERTs trotz ON CONFLICT DO NOTHING

Datentypen und Umwandlungen

Vorhandene Tabelle als Vorlage für Datentypen ...

Explizite Typumwandlungen für die erste Datenzeile in den freistehenden VALUES Ausdruck kann unbequem sein. Es gibt Möglichkeiten, es zu umgehen. Als Zeilenvorlage kann jede vorhandene Relation (Tabelle, View, ...) verwendet werden. Die Zieltabelle ist die offensichtliche Wahl für den Anwendungsfall. Eingabedaten werden automatisch in geeignete Typen umgewandelt, wie in den 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. Siehe:

  • Umwandeln des NULL-Typs beim Aktualisieren mehrerer Zeilen

... 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 Tisch chats im Beispiel besteht nur aus den 3 Spalten, die im UPSERT verwendet werden:

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

Übrigens:Verwenden Sie keine reservierten Wörter wie "user" als Kennung. Das ist eine geladene Fußwaffe. Verwenden Sie zulässige Bezeichner in Kleinbuchstaben und ohne Anführungszeichen. Ich habe es durch usr ersetzt .