Wie verwende ich RETURNING mit ON CONFLICT in PostgreSQL?


149

Ich habe das folgende UPSERT in PostgreSQL 9.5:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

Wenn es keine Konflikte gibt, wird Folgendes zurückgegeben:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Bei Konflikten werden jedoch keine Zeilen zurückgegeben:

----------
    | id |
----------

Ich möchte die neuen idSpalten zurückgeben, wenn keine Konflikte vorliegen, oder die vorhandenen idSpalten der widersprüchlichen Spalten zurückgeben.
Kann das gemacht werden? Wenn ja, wie?


1
Verwenden Sie diese Option, ON CONFLICT UPDATEdamit die Zeile geändert wird. Dann RETURNINGwird es erfasst.
Gordon Linoff

1
@ GordonLinoff Was ist, wenn es nichts zu aktualisieren gibt?
Okku

1
Wenn es nichts zu aktualisieren gibt, bedeutet dies, dass kein Konflikt
aufgetreten ist.

1
Sie werden andere Wege finden hier . Ich würde gerne den Unterschied zwischen den beiden in Bezug auf die Leistung kennen.
Stanislasdrg Reinstate Monica

Antworten:


88

Ich hatte genau das gleiche Problem und löste es mit "Update durchführen" anstelle von "Nichts tun", obwohl ich nichts zu aktualisieren hatte. In Ihrem Fall wäre es ungefähr so:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Diese Abfrage gibt alle Zeilen zurück, unabhängig davon, ob sie gerade eingefügt wurden oder bereits vorhanden waren.


11
Ein Problem bei diesem Ansatz besteht darin, dass die Sequenznummer des Primärschlüssels bei jedem Konflikt erhöht wird (falsche Aktualisierung), was im Grunde bedeutet, dass Sie möglicherweise große Lücken in der Sequenz haben. Irgendwelche Ideen, wie man das vermeidet?
Mischa

9
@ Mischa: na und? Sequenzen sind niemals garantiert lückenlos und Lücken spielen keine Rolle (und wenn doch, ist eine Sequenz das Falsche)
a_horse_with_no_name

24
Ich würde in den meisten Fällen nicht empfehlen, dies zu verwenden. Ich habe eine Antwort hinzugefügt, warum.
Erwin Brandstetter

4
Diese Antwort scheint nicht den DO NOTHINGAspekt der ursprünglichen Frage zu erreichen - für mich scheint sie das konfliktfreie Feld (hier "Name") für alle Zeilen zu aktualisieren.
PeterJCLaw

Wie in der sehr langen Antwort unten erläutert, ist die Verwendung von "Update durchführen" für ein Feld, das sich nicht geändert hat, keine "saubere" Lösung und kann andere Probleme verursachen.
Bill Worthington

202

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 VACUUMKosten. 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 UPDATEein conflict_targetmuss 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 sourceSpalte 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 chatsfunktioniert, 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 VALUESAusdruck 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 VALUESVerwendung in INSERTwerden 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 COMMITTEDannehmen . 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), INSERTerkennen Sie einen Konflikt (der UNIQUEIndex / die Einschränkung ist absolut) und geben DO NOTHINGdaher 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 SELECTsieht 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 SELECTals 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 VALUESAusdruck 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 VALUESKlausel 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 chatsim 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.


2
Sie implizieren, dass diese Methode keine Lücken in den Serien erzeugt, aber sie sind: INSERT ... ON CONFLICT DO NOTHING erhöht die Serie jedes Mal von dem, was ich sehen kann
schädlich

1
nicht, dass es so wichtig wäre, aber warum werden Serien erhöht? und gibt es keine Möglichkeit, dies zu vermeiden?
Auffallend

1
@salient: Wie ich oben hinzugefügt habe: Spaltenstandardwerte werden vor dem Testen auf Konflikte ausgefüllt und Sequenzen werden niemals zurückgesetzt, um Konflikte mit gleichzeitigen Schreibvorgängen zu vermeiden.
Erwin Brandstetter

7
Unglaublich. Funktioniert wie ein Zauber und ist leicht zu verstehen, wenn Sie es sich genau ansehen. Ich wünsche mir immer noch, ON CONFLICT SELECT...wo etwas :)
Roshambo

3
Unglaublich. Die Schöpfer von Postgres scheinen Benutzer zu foltern. Warum nicht einfach die Rückgabeklausel immer Werte zurückgeben lassen, unabhängig davon, ob Einfügungen vorhanden waren oder nicht?
Anatoly Alekseev

16

Upsert, eine Erweiterung der INSERTAbfrage zu sein, kann im Falle eines Einschränkungskonflikts mit zwei verschiedenen Verhaltensweisen definiert werden: DO NOTHINGoder DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Beachten Sie auch, dass RETURNINGnichts zurückgegeben wird, da keine Tupel eingefügt wurden . Mit DO UPDATEist es nun möglich, Operationen an dem Tupel auszuführen, mit dem ein Konflikt besteht. Beachten Sie zunächst, dass es wichtig ist, eine Einschränkung zu definieren, anhand derer definiert wird, dass ein Konflikt vorliegt.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
Ein guter Weg, um immer die betroffene Zeilen-ID zu erhalten und zu wissen, ob es sich um eine Einfügung oder eine Upsert handelt. Genau das, was ich brauchte.
Moby Duck

Dies verwendet immer noch das "Do Update", dessen Nachteile bereits besprochen wurden.
Bill Worthington

4

Für das Einfügen eines einzelnen Elements würde ich bei der Rückgabe der ID wahrscheinlich eine Koaleszenz verwenden:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

Der Hauptzweck der Verwendung ON CONFLICT DO NOTHINGbesteht darin, Fehler zu vermeiden, aber es werden keine Zeilenrückgaben verursacht. Also brauchen wir noch einenSELECT , um die vorhandene ID zu erhalten.

Wenn es in dieser SQL bei Konflikten fehlschlägt, wird nichts zurückgegeben, und die zweite SELECTZeile erhält die vorhandene Zeile. Wenn es erfolgreich eingefügt wird, gibt es zwei gleiche Datensätze, dann müssen wir UNIONdas Ergebnis zusammenführen.


Diese Lösung funktioniert gut und vermeidet unnötiges Schreiben (Aktualisieren) in die Datenbank !! Nett!
Simon C

0

Ich habe die erstaunliche Antwort von Erwin Brandstetter geändert, die die Sequenz nicht erhöht und auch keine Zeilen schreibgeschützt. Ich bin relativ neu in PostgreSQL. Bitte lassen Sie mich wissen, wenn Sie Nachteile dieser Methode feststellen:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

Dies setzt voraus, dass die Tabelle chats eine eindeutige Einschränkung für Spalten aufweist (usr, contact).

Update: Die vorgeschlagenen Überarbeitungen von Spatar (unten) wurden hinzugefügt . Vielen Dank!


1
Anstatt CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existsnur zu schreiben r.id IS NOT NULL as row_exists. Anstatt WHERE row_exists=FALSEnur zu schreiben WHERE NOT row_exists.
Spatar
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.