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

So schließen Sie ausgeschlossene Zeilen in RETURNING von INSERT ... ON CONFLICT ein

Der Fehler, den Sie erhalten:

Der Befehl ON CONFLICT DO UPDATE kann die Zeile nicht ein zweites Mal beeinflussen

gibt an, dass Sie versuchen, dieselbe Zeile mehr als einmal in einem einzigen Befehl zu aktualisieren. Mit anderen Worten:Sie haben Duplikate auf (name, url, email) in Ihren VALUES aufführen. Duplikate falten (falls das eine Option ist) und es sollte funktionieren. Aber Sie müssen entscheiden, welche Reihe Sie aus jedem Satz von Duplikaten auswählen möchten.

INSERT INTO feeds_person (created, modified, name, url, email)
SELECT DISTINCT ON (name, url, email) *
FROM  (
   VALUES
   ('blah', 'blah', 'blah', 'blah', 'blah')
   -- ... more
   ) v(created, modified, name, url, email)  -- match column list
ON     CONFLICT (name, url, email) DO UPDATE
SET    url = feeds_person.url
RETURNING id;

Da wir einen freistehenden VALUES verwenden -Ausdruck jetzt müssen Sie explizite Typumwandlungen für nicht standardmäßige Typen hinzufügen. Wie:

VALUES
    (timestamptz '2016-03-12 02:47:56+01'
   , timestamptz '2016-03-12 02:47:56+01'
   , 'n3', 'u3', 'e3')
   ...

Ihr timestamptz Spalten benötigen eine explizite Typumwandlung, während die String-Typen mit dem Standardwert text arbeiten können . (Sie könnten immer noch in varchar(n) umwandeln sofort.)

Es gibt Möglichkeiten, um zu bestimmen, welche Zeile aus jedem Satz von Duplikaten ausgewählt werden soll:

  • Erste Zeile in jeder GROUP BY-Gruppe auswählen?

Sie haben Recht, es gibt (derzeit) keine Möglichkeit, ausgeschlossen zu werden Zeilen in RETURNING Klausel. Ich zitiere das Postgres-Wiki:

Beachten Sie, dass RETURNING macht das "EXCLUDED.*" nicht sichtbar " Alias ​​aus dem UPDATE (nur das generische "TARGET.* " Alias ​​ist dort sichtbar). Es wird angenommen, dass dies zu lästigen Mehrdeutigkeiten für die einfachen, häufigen Fälle [30] mit wenig bis gar keinem Nutzen führt. Irgendwann in der Zukunft werden wir vielleicht einen Weg verfolgen, um aufzudecken, obRETURNING -projizierte Tupel wurden eingefügt und aktualisiert, aber das muss es wahrscheinlich nicht in die erste festgeschriebene Iteration des Features [31] schaffen.

Allerdings , sollten Sie keine Zeilen aktualisieren, die nicht aktualisiert werden sollen. Leere Updates sind fast so teuer wie normale Updates – und können unbeabsichtigte Nebeneffekte haben. Sie brauchen UPSERT nicht unbedingt, Ihr Fall sieht eher aus wie "SELECT or INSERT". Verwandte:

  • Ist SELECT oder INSERT in einer Funktion anfällig für Race-Conditions?

Einer Eine sauberere Methode zum Einfügen einer Reihe von Zeilen wäre die Verwendung von datenmodifizierenden CTEs:

WITH val AS (
   SELECT DISTINCT ON (name, url, email) *
   FROM  (
      VALUES 
      (timestamptz '2016-1-1 0:0+1', timestamptz '2016-1-1 0:0+1', 'n', 'u', 'e')
    , ('2016-03-12 02:47:56+01', '2016-03-12 02:47:56+01', 'n1', 'u3', 'e3')
      -- more (type cast only needed in 1st row)
      ) v(created, modified, name, url, email)
   )
, ins AS (
   INSERT INTO feeds_person (created, modified, name, url, email)
   SELECT created, modified, name, url, email FROM val
   ON     CONFLICT (name, url, email) DO NOTHING
   RETURNING id, name, url, email
   )
SELECT 'inserted' AS how, id FROM ins  -- inserted
UNION  ALL
SELECT 'selected' AS how, f.id         -- not inserted
FROM   val v
JOIN   feeds_person f USING (name, url, email);

Die zusätzliche Komplexität sollte sich für große Tabellen auszahlen, in denen INSERT ist die Regel und SELECT die Ausnahme.

Ursprünglich hatte ich einen NOT EXISTS hinzugefügt Prädikat auf dem letzten SELECT um Duplikate im Ergebnis zu vermeiden. Aber das war überflüssig. Alle CTEs einer einzelnen Abfrage sehen dieselben Snapshots von Tabellen. Das mit ON CONFLICT (name, url, email) DO NOTHING zurückgegebene Set schließen sich gegenseitig mit dem Satz aus, der nach dem INNER JOIN zurückgegeben wird in denselben Spalten.

Leider öffnet dies auch ein winziges Fenster für eine Rennbedingung . Wenn ...

  • Eine gleichzeitige Transaktion fügt widersprüchliche Zeilen ein
  • hat noch keine Zusage gemacht
  • aber übergibt schließlich

... einige Zeilen können verloren gehen.

Sie könnten einfach INSERT .. ON CONFLICT DO NOTHING , gefolgt von einem separaten SELECT Abfrage für alle Zeilen - innerhalb derselben Transaktion, um dies zu überwinden. Was wiederum ein weiteres winziges Fenster für eine Rennbedingung öffnet wenn gleichzeitige Transaktionen Schreibvorgänge in die Tabelle zwischen INSERT festschreiben können und SELECT (standardmäßig READ COMMITTED Isolationsstufe). Kann mit REPEATABLE READ vermieden werden Transaktionsisolation (oder strenger). Oder mit einer (möglicherweise teuren oder sogar inakzeptablen) Schreibsperre auf der gesamten Tabelle. Sie können jedes Verhalten bekommen, das Sie brauchen, aber es kann sein, dass Sie dafür einen Preis zahlen müssen.

Verwandte:

  • Wie verwendet man RETURNING mit ON CONFLICT in PostgreSQL?
  • Zeilen von INSERT mit ON CONFLICT zurückgeben, ohne aktualisieren zu müssen