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
oderIMMUTABLE
Standardausdrücke . Die meistenVOLATILE
Funktionen werden genauso gut funktionieren, aber es gibt keine Garantien. Dercurrent_timestamp
Familie von Funktionen gelten als stabil, da sich ihre Werte innerhalb einer Transaktion nicht ändern.
Insbesondere hat dies Seiteneffekte aufserial
Spalten (oder andere Standardwerte, die aus einer Sequenz stammen). Das sollte aber kein Problem sein, da man normalerweise nicht inserial
schreibt Spalten direkt. Diese sollten nicht inINSERT
aufgeführt werden Aussagen überhaupt.
Verbleibender Fehler fürserial
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 inserial
Spalten.
Zwei weitere Probleme können gelöst werden:
-
Wenn Sie Spalten definiert haben
NOT NULL
, müssen Sie Dummy-Werte einfügen und durchNULL
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 zumUPDATE
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.