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
STABLEoIMMUTABLEespressioni predefinite . La maggior parte deiVOLATILEle funzioni funzioneranno altrettanto bene, ma non ci sono garanzie. Ilcurrent_timestampfamiglia di funzioni si qualificano come stabili, poiché i loro valori non cambiano all'interno di una transazione.
In particolare, ciò ha effetti collaterali suserialcolonne (o qualsiasi altro valore predefinito che attinge da una sequenza). Ma questo non dovrebbe essere un problema, perché normalmente non scrivi suserialcolonne direttamente. Quelli non dovrebbero essere elencati inINSERTdichiarazioni.
Difetto rimanente perserialcolonne: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 inserialcolonne.
Si possono risolvere altri due problemi:
-
Se hai colonne definite
NOT NULL, devi inserire valori fittizi e sostituirli conNULLnel 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
WHEREclausola alUPDATEper 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.