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

Generieren Sie DEFAULT-Werte in einem CTE UPSERT mit PostgreSQL 9.3

Postgres 9.5 hat UPSERT implementiert . Siehe unten.

Postgres 9.4 oder älter

Dies ist ein kniffliges Problem. Sie stoßen auf diese Einschränkung (laut Dokumentation):

In einem VALUES Liste, die auf der obersten Ebene eines INSERT erscheint , kann ein Ausdruck durch DEFAULT ersetzt werden um anzugeben, dass der Standardwert der Zielspalte eingefügt werden soll. DEFAULT kann nicht verwendet werden, wennVALUES erscheint in anderen Zusammenhängen.

Fette Hervorhebung von mir. Standardwerte werden nicht ohne eine einzufügende Tabelle definiert. Es gibt also kein direktes Lösung für Ihre Frage, aber es gibt eine Reihe möglicher alternativer Routen, abhängig von den genauen Anforderungen .

Standardwerte aus Systemkatalog abrufen?

Sie könnten Holen Sie sich diese aus dem Systemkatalog pg_attrdef wie @Patrick kommentiert oder aus information_schema.columns . Vollständige Anweisungen hier:

  • Standardwerte von Tabellenspalten in Postgres erhalten?

Aber dann bist du noch haben nur eine Liste von Zeilen mit einer Textdarstellung des Ausdrucks, um den Standardwert zu kochen. Sie müssten Anweisungen dynamisch erstellen und ausführen, um Werte zu erhalten, mit denen Sie arbeiten können. Mühsam und chaotisch. Stattdessen können wir die integrierte Postgres-Funktionalität für uns erledigen :

Einfache Verknüpfung

Fügen Sie eine Dummy-Zeile ein und lassen Sie sie zurückgeben, um generierte Standardwerte zu verwenden:

INSERT INTO playlist_items DEFAULT VALUES RETURNING *;

Probleme / Umfang der Lösung

  • Dies funktioniert garantiert nur für STABLE oder IMMUTABLE Standardausdrücke . Die meisten VOLATILE Funktionen werden genauso gut funktionieren, aber es gibt keine Garantien. Der current_timestamp Familie von Funktionen gelten als stabil, da sich ihre Werte innerhalb einer Transaktion nicht ändern.
    Insbesondere hat dies Seiteneffekte auf serial Spalten (oder andere Standardwerte, die aus einer Sequenz stammen). Das sollte aber kein Problem sein, da man normalerweise nicht in serial schreibt Spalten direkt. Diese sollten nicht in INSERT aufgeführt werden Aussagen überhaupt.
    Verbleibender Fehler für serial Spalten:Die Sequenz wird immer noch um den einzelnen Aufruf erweitert, um eine Standardzeile zu erhalten, wodurch eine Lücke in der Nummerierung entsteht. Auch das sollte kein Problem sein, da Lücken im Allgemeinen zu erwarten sind in serial Spalten.

Zwei weitere Probleme können gelöst werden:

  • Wenn Sie Spalten definiert haben NOT NULL , müssen Sie Dummy-Werte einfügen und durch NULL ersetzen im Ergebnis.

  • Wir wollen die Dummy-Zeile eigentlich nicht einfügen . Wir könnten später (in derselben Transaktion) löschen, aber das könnte mehr Nebeneffekte haben, wie Trigger ON DELETE . Es gibt einen besseren Weg:

Dummy-Zeile vermeiden

Klonen Sie eine temporäre Tabelle inklusive Spaltenvorgaben und einfügen in das :

BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
   ON COMMIT DROP;  -- drop at end of transaction

INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...

Gleiches Ergebnis, weniger Nebenwirkungen. Da Standardausdrücke wörtlich kopiert werden, schöpft der Klon aus denselben Sequenzen, falls vorhanden. Aber auch andere Nebeneffekte aus der ungewollten Reihe oder Trigger werden komplett vermieden.

Danke an Igor für die Idee:

  • Postgresql, wähle eine "gefälschte" Zeile aus

Entfernen Sie NOT NULL Einschränkungen

Sie müssten Dummy-Werte für NOT NULL bereitstellen Spalten, weil (laut Dokumentation):

Nicht-Null-Constraints werden immer in die neue Tabelle kopiert.

Entweder diese im INSERT unterbringen Anweisung oder (besser) eliminieren Sie die Einschränkungen:

ALTER TABLE tmp_playlist_items
   ALTER COLUMN foo DROP NOT NULL
 , ALTER COLUMN bar DROP NOT NULL;

Es gibt einen schnellen und schmutzigen Weg mit Superuser-Rechten:

UPDATE pg_attribute
SET    attnotnull = FALSE
WHERE  attrelid = 'tmp_playlist_items'::regclass
AND    attnotnull
AND    attnum > 0;

Es ist nur eine temporäre Tabelle ohne Daten und ohne anderen Zweck und wird am Ende der Transaktion gelöscht. Die Abkürzung ist also verlockend. Dennoch gilt die Grundregel:Manipulieren Sie Systemkataloge niemals direkt.

Sehen wir uns also einen sauberen Weg an :Mit dynamischem SQL in einem DO automatisieren Erklärung. Sie benötigen lediglich die normalen Berechtigungen Sie haben es garantiert, da dieselbe Rolle die temporäre Tabelle erstellt hat.

DO $$BEGIN
EXECUTE (
   SELECT 'ALTER TABLE tmp_playlist_items ALTER '
       || string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
       || ' DROP NOT NULL'
   FROM   pg_catalog.pg_attribute
   WHERE  attrelid = 'tmp_playlist_items'::regclass
   AND    attnotnull
   AND    attnum > 0
   );
END$$

Viel sauberer und trotzdem sehr schnell. Gehen Sie vorsichtig mit dynamischen Befehlen um und seien Sie vorsichtig bei der SQL-Einschleusung. Diese Aussage ist sicher. Ich habe mehrere verwandte Antworten mit weiteren Erläuterungen gepostet.

Allgemeine Lösung (9.4 und älter)

BEGIN;

CREATE TEMP TABLE tmp_playlist_items
   (LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;

DO $$BEGIN
EXECUTE (
   SELECT 'ALTER TABLE tmp_playlist_items ALTER '
       || string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
       || ' DROP NOT NULL'
   FROM   pg_catalog.pg_attribute
   WHERE  attrelid = 'tmp_playlist_items'::regclass
   AND    attnotnull
   AND    attnum > 0
   );
END$$;

LOCK TABLE playlist_items IN EXCLUSIVE MODE;  -- forbid concurrent writes

WITH default_row AS (
   INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
   )
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
   VALUES
      (651, 21, 30012, 'a', 30, 1, FALSE)
    , (NULL, 21, 1, 'b', 34, 2, NULL)
    , (668, 21, 30012, 'c', 30, 3, FALSE)
    , (7428, 21, 23068, 'd', 0, 4, FALSE)
   )
, upsert AS (  -- *not* replacing existing values in UPDATE (?)
   UPDATE playlist_items m
   SET   (  playlist,   item,   group_name,   duration,   sort,   legacy)
       = (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
   --                                   ..., COALESCE(n.legacy, m.legacy)  -- see below
   FROM   new_values n
   WHERE  n.id = m.id
   RETURNING m.id
   )
INSERT INTO playlist_items
        (playlist,   item,   group_name,   duration,   sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
                                   , COALESCE(n.legacy, d.legacy)
FROM   new_values n, default_row d   -- single row can be cross-joined
WHERE  NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;

COMMIT;

Sie brauchen nur das LOCK wenn Sie gleichzeitige Transaktionen haben, die versuchen, in dieselbe Tabelle zu schreiben.

Dies ersetzt wie gewünscht nur NULL-Werte in der Spalte legacy in den Eingabezeilen für INSERT Fall. Kann leicht erweitert werden, um für andere Spalten oder im UPDATE zu arbeiten Fall ebenso. Zum Beispiel könnten Sie UPDATE auch bedingt:nur wenn der Eingabewert NOT NULL ist . Ich habe dem UPDATE eine kommentierte Zeile hinzugefügt oben.

Übrigens:Du musst nicht casten Werte in jeder Zeile außer dem ersten in einem VALUES Ausdruck, da Typen vom ersten abgeleitet werden Zeile.

Postgres 9.5

implementiert UPSERT mit INSERT .. ON CONFLICT .. DO NOTHING | UPDATE . Dies vereinfacht die Bedienung erheblich:

INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
,      (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT)  -- !
,      (668, 21, 30012, 'c', 30, 3, FALSE)
,      (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
 = (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
  , EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (...,  COALESCE(l.legacy, EXCLUDED.legacy))  -- see below
RETURNING m.id;

Wir können die VALUES anhängen -Klausel zu INSERT direkt, was den DEFAULT erlaubt Stichwort. Bei eindeutigen Verstößen gegen (id) , Postgres-Updates stattdessen. Wir können ausgeschlossene Zeilen im UPDATE verwenden . Das Handbuch:

Das SET und WHERE Klauseln in ON CONFLICT DO UPDATE haben Zugriff auf die vorhandene Zeile unter Verwendung des Tabellennamens (oder eines Alias) und auf Zeilen, die zum Einfügen vorgeschlagen werden, unter Verwendung des speziellen excluded Tabelle.

Und:

Beachten Sie, dass die Auswirkungen aller pro Zeile BEFORE INSERT Auslöser spiegeln sich in ausgeschlossenen Werten wider, da diese Effekte möglicherweise dazu beigetragen haben, dass die Zeile vom Einfügen ausgeschlossen wurde.

Verbleibender Eckfall

Für das UPDATE haben Sie verschiedene Möglichkeiten :Sie können ...

  • ... überhaupt nicht aktualisieren:Fügen Sie ein WHERE hinzu -Klausel zum UPDATE um nur in ausgewählte Zeilen zu schreiben.
  • ... nur ausgewählte Spalten aktualisieren.
  • ... nur aktualisieren, wenn die Spalte derzeit NULL ist:COALESCE(l.legacy, EXCLUDED.legacy)
  • ... nur aktualisieren, wenn der neue Wert NOT NULL ist :COALESCE(EXCLUDED.legacy, l.legacy)

Aber es gibt keine Möglichkeit, DEFAULT zu erkennen Werte und tatsächlich in INSERT bereitgestellte Werte . Nur resultierender EXCLUDED Reihen sichtbar. Wenn Sie die Unterscheidung benötigen, greifen Sie auf die vorherige Lösung zurück, wo Ihnen beides zur Verfügung steht.