Postgres 9.5 ha implementato UPSERT
. Vedi sotto.
Postgres 9.4 o precedente
Questo è un problema complicato. Stai riscontrando questa restrizione (per documentazione):
In un VALUES
elenco visualizzato al livello superiore di un INSERT
, un'espressione può essere sostituita da DEFAULT
per indicare che deve essere inserito il valore predefinito della colonna di destinazione. DEFAULT
non può essere utilizzato quandoVALUES
appare in altri contesti.
Enfasi in grassetto mio. I valori predefiniti non sono definiti senza una tabella in cui inserirli. Quindi non esiste un diretto soluzione alla tua domanda, ma ci sono una serie di possibili percorsi alternativi, a seconda dei requisiti esatti .
Recuperare i valori predefiniti dal catalogo di sistema?
Potresti recupera quelli dal catalogo di sistema pg_attrdef
come ha commentato @Patrick o da information_schema.columns
. Completare le istruzioni qui:
- Ottieni i valori predefiniti delle colonne della tabella in Postgres?
Ma poi ancora avere solo un elenco di righe con una rappresentazione testuale dell'espressione per cucinare il valore predefinito. Dovresti creare ed eseguire istruzioni in modo dinamico per ottenere valori con cui lavorare. Noioso e disordinato. Invece, possiamo lasciare che la funzionalità Postgres incorporata lo faccia per noi :
Semplice scorciatoia
Inserisci una riga fittizia e falla tornare per utilizzare i valori predefiniti generati:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Problemi/ambito della soluzione
- Questo è garantito per funzionare solo per
STABLE
oIMMUTABLE
espressioni predefinite . La maggior parte deiVOLATILE
le funzioni funzioneranno altrettanto bene, ma non ci sono garanzie. Ilcurrent_timestamp
famiglia di funzioni si qualificano come stabili, poiché i loro valori non cambiano all'interno di una transazione.
In particolare, ciò ha effetti collaterali suserial
colonne (o qualsiasi altro valore predefinito che attinge da una sequenza). Ma questo non dovrebbe essere un problema, perché normalmente non scrivi suserial
colonne direttamente. Quelli non dovrebbero essere elencati inINSERT
dichiarazioni.
Difetto rimanente perserial
colonne:la sequenza viene comunque avanzata dalla singola chiamata per ottenere una riga di default, producendo una lacuna nella numerazione. Anche in questo caso, questo non dovrebbe essere un problema, perché generalmente ci si aspettano lacune inserial
colonne.
Si possono risolvere altri due problemi:
-
Se hai colonne definite
NOT NULL
, devi inserire valori fittizi e sostituirli conNULL
nel risultato. -
In realtà non vogliamo inserire la riga fittizia . Potremmo eliminare in seguito (nella stessa transazione), ma ciò potrebbe avere più effetti collaterali, come i trigger
ON DELETE
. C'è un modo migliore:
Evita la fila fittizia
Clona una tabella temporanea inclusi i valori predefiniti delle colonne e inserirli in quello :
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 *;
...
Stesso risultato, meno effetti collaterali. Poiché le espressioni predefinite vengono copiate alla lettera, il clone disegna dalle stesse sequenze, se presenti. Ma altri effetti collaterali della riga o dei trigger indesiderati vengono completamente evitati.
Ringraziamo Igor per l'idea:
- Postgresql, seleziona una riga "falsa"
Rimuovi NOT NULL
vincoli
Dovresti fornire valori fittizi per NOT NULL
colonne, perché (per documentazione):
I vincoli non nulli vengono sempre copiati nella nuova tabella.
O accomoda quelli nel INSERT
dichiarazione o (meglio) eliminare i vincoli:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
C'è un modo rapido e sporco con privilegi di superutente:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
È solo una tabella temporanea senza dati e nessun altro scopo e viene eliminata alla fine della transazione. Quindi la scorciatoia è allettante. Tuttavia, la regola di base è:non manomettere mai direttamente i cataloghi di sistema.
Quindi, esaminiamo un modo pulito :Automatizza con SQL dinamico in un DO
dichiarazione. Hai solo bisogno dei privilegi regolari hai la garanzia di avere poiché lo stesso ruolo ha creato la tabella temporanea.
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$$
Molto più pulito e comunque molto veloce. Esegui attenzione con i comandi dinamici e fai attenzione all'iniezione SQL. Questa affermazione è sicura. Ho pubblicato diverse risposte correlate con ulteriori spiegazioni.
Soluzione generale (9.4 e precedenti)
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;
Hai solo bisogno del LOCK
se hai transazioni simultanee che tentano di scrivere nella stessa tabella.
Come richiesto, questo sostituisce solo i valori NULL nella colonna legacy
nelle righe di input per INSERT
Astuccio. Può essere facilmente esteso per funzionare per altre colonne o in UPDATE
anche il caso. Ad esempio, potresti UPDATE
anche condizionatamente:solo se il valore di input è NOT NULL
. Ho aggiunto una riga commentata a UPDATE
sopra.
A parte:non è necessario trasmettere valori in qualsiasi riga tranne la prima in un VALUES
espressione, poiché i tipi derivano dal primo riga.
Postgres 9.5
implementa UPSERT con INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Questo semplifica ampiamente l'operazione:
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;
Possiamo allegare i VALUES
clausola su INSERT
direttamente, che consente il DEFAULT
parola chiave. In caso di violazioni uniche su (id)
, Postgres aggiorna invece. Possiamo utilizzare le righe escluse in UPDATE
. Il manuale:
Il SET
e WHERE
clausole in ON CONFLICT DO UPDATE
avere accesso alla riga esistente utilizzando il nome della tabella (o un alias) e alle righe proposte per l'inserimento utilizzando l'apposito excluded
tabella.
E:
Nota che gli effetti di tutti i BEFORE INSERT
per riga i trigger si riflettono nei valori esclusi, poiché tali effetti potrebbero aver contribuito all'esclusione della riga dall'inserimento.
Custodia angolare rimanente
Hai varie opzioni per UPDATE
:Puoi...
- ... non aggiorna affatto:aggiungi un
WHERE
clausola alUPDATE
per scrivere solo nelle righe selezionate. - ... aggiorna solo le colonne selezionate.
- ... aggiorna solo se la colonna è attualmente NULL:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... aggiorna solo se il nuovo valore è
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Ma non c'è modo di distinguere DEFAULT
valori e valori effettivamente forniti in INSERT
. Solo risultante EXCLUDED
le righe sono visibili. Se hai bisogno della distinzione, torna alla soluzione precedente, dove hai entrambi a nostra disposizione.