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

Wie aktualisiere ich alle Spalten mit INSERT ... ON CONFLICT ...?

Das UPDATE Syntax erfordert Zielspalten explizit zu benennen. Mögliche Gründe, dies zu vermeiden:

  • Sie haben viele Spalten und möchten nur die Syntax kürzen.
  • Du weißt es nicht Spaltennamen mit Ausnahme der eindeutigen Spalte(n).

"All columns" muss "alle Spalten der Zieltabelle" bedeuten (oder zumindest "führende Spalten der Tabelle" ) in übereinstimmender Reihenfolge und übereinstimmendem Datentyp. Andernfalls müssten Sie sowieso eine Liste mit Zielspaltennamen bereitstellen.

Testtabelle:

CREATE TABLE tbl (
   id    int PRIMARY KEY
 , text  text
 , extra text
);

INSERT INTO tbl AS t
VALUES (1, 'foo')
     , (2, 'bar');

1. DELETE &INSERT stattdessen in Einzelabfrage

Ohne irgendwelche Spaltennamen außer id zu kennen .

Funktioniert nur für "alle Spalten der Zieltabelle" . Während die Syntax sogar für eine führende Teilmenge funktioniert, würden überzählige Spalten in der Zieltabelle mit DELETE auf NULL zurückgesetzt und INSERT .

UPSERT (INSERT ... ON CONFLICT ... ) wird benötigt, um Parallelitäts-/Sperrprobleme bei gleichzeitiger Schreiblast zu vermeiden, und nur, weil es keine allgemeine Möglichkeit gibt, noch nicht vorhandene Zeilen in Postgres zu sperren (Wertsperre ).

Ihre spezielle Anforderung betrifft nur das UPDATE Teil. Mögliche Komplikationen gelten nicht, wenn vorhanden Reihen sind betroffen. Diese sind ordnungsgemäß verschlossen. Um es noch weiter zu vereinfachen, können Sie Ihren Fall auf DELETE reduzieren und INSERT :

WITH data(id) AS (              -- Only 1st column gets explicit name!
   VALUES
      (1, 'foo_upd', 'a')       -- changed
    , (2, 'bar', 'b')           -- unchanged
    , (3, 'baz', 'c')           -- new
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data d
   WHERE  t.id = d.id
   -- AND    t <> d              -- optional, to avoid empty updates
   )                             -- only works for complete rows
INSERT INTO tbl AS t
TABLE  data                      -- short for: SELECT * FROM data
ON     CONFLICT (id) DO NOTHING
RETURNING t.id;

Im Postgres-MVCC-Modell ein UPDATE ist weitgehend dasselbe wie DELETE und INSERT sowieso (mit Ausnahme einiger Eckfälle mit Parallelität, HOT-Updates und großen Spaltenwerten, die außerhalb der Reihe gespeichert werden). Da Sie sowieso alle Zeilen ersetzen möchten, entfernen Sie einfach widersprüchliche Zeilen vor dem INSERT . Gelöschte Zeilen bleiben gesperrt, bis die Transaktion festgeschrieben wird. Das INSERT möglicherweise nur widersprüchliche Zeilen für zuvor nicht vorhandene Schlüsselwerte finden, wenn eine gleichzeitige Transaktion sie gleichzeitig einfügt (nach dem DELETE , aber vor dem INSERT ).

In diesem speziellen Fall würden Sie zusätzliche Spaltenwerte für betroffene Zeilen verlieren. Keine Ausnahme ausgelöst. Aber wenn konkurrierende Anfragen die gleiche Priorität haben, ist das kaum ein Problem:Die andere Anfrage hat für einige gewonnen Reihen. Wenn die andere Abfrage ein ähnlicher UPSERT ist, besteht die Alternative darin, auf die Festschreibung dieser Transaktion zu warten und sie dann sofort zu aktualisieren. "Gewinnen" könnte ein Pyrrhussieg sein.

Über "leere Updates":

  • Wie wähle ich (oder kann ich) DISTINCT für mehrere Spalten aus?

Nein, meine Anfrage muss gewinnen!

OK, Sie haben danach gefragt:

WITH data(id) AS (                   -- Only 1st column gets explicit name!
   VALUES                            -- rest gets default names "column2", etc.
     (1, 'foo_upd', NULL)              -- changed
   , (2, 'bar', NULL)                  -- unchanged
   , (3, 'baz', NULL)                  -- new
   , (4, 'baz', NULL)                  -- new
   )
, ups AS (
   INSERT INTO tbl AS t
   TABLE  data                       -- short for: SELECT * FROM data
   ON     CONFLICT (id) DO UPDATE
   SET    id = t.id
   WHERE  false                      -- never executed, but locks the row!
   RETURNING t.id
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data     d
   LEFT   JOIN ups u USING (id)
   WHERE  u.id IS NULL               -- not inserted !
   AND    t.id = d.id
   -- AND    t <> d                  -- avoid empty updates - only for full rows
   RETURNING t.id
   )
, ins AS (
   INSERT INTO tbl AS t
   SELECT *
   FROM   data
   JOIN   del USING (id)             -- conflict impossible!
   RETURNING id
   )
SELECT ARRAY(TABLE ups) AS inserted  -- with UPSERT
     , ARRAY(TABLE ins) AS updated   -- with DELETE & INSERT;

Wie?

  • Die 1. CTE data liefert nur Daten. Könnte stattdessen eine Tabelle sein.
  • Die 2. CTE ups :UPSERT. Zeilen mit widersprüchlicher id nicht verändert, sondern auch gesperrt .
  • Der 3. CTE del löscht widersprüchliche Zeilen. Sie bleiben gesperrt.
  • Der 4. CTE ins fügt ganze Zeilen ein . Nur für dieselbe Transaktion zulässig
  • Das letzte SELECT ist nur für die Demo, um zu zeigen, was passiert ist.

Um nach leeren Updates zu suchen, testen Sie (vorher und nachher) mit:

SELECT ctid, * FROM tbl; -- did the ctid change?

Die (auskommentierte) Prüfung auf Änderungen in der Zeile AND t <> d funktioniert auch mit NULL-Werten, da wir laut Handbuch zwei typisierte Zeilenwerte vergleichen:

zwei NULL-Feldwerte werden als gleich angesehen, und eine NULL wird als größer als eine Nicht-NULL betrachtet

2. Dynamisches SQL

Dies funktioniert auch für eine Teilmenge führender Spalten, wobei vorhandene Werte erhalten bleiben.

Der Trick besteht darin, Postgres die Abfragezeichenfolge mit Spaltennamen aus den Systemkatalogen dynamisch erstellen und dann ausführen zu lassen.

Siehe verwandte Antworten für Code:

  • Aktualisieren Sie mehrere Spalten in einer Trigger-Funktion in plpgsql

  • Massenaktualisierung aller Spalten

  • SQL aktualisiert Felder einer Tabelle aus Feldern einer anderen