PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Genera valori DEFAULT in un CTE UPSERT usando PostgreSQL 9.3

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 o IMMUTABLE espressioni predefinite . La maggior parte dei VOLATILE le funzioni funzioneranno altrettanto bene, ma non ci sono garanzie. Il current_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 su serial colonne (o qualsiasi altro valore predefinito che attinge da una sequenza). Ma questo non dovrebbe essere un problema, perché normalmente non scrivi su serial colonne direttamente. Quelli non dovrebbero essere elencati in INSERT dichiarazioni.
    Difetto rimanente per serial 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 in serial colonne.

Si possono risolvere altri due problemi:

  • Se hai colonne definite NOT NULL , devi inserire valori fittizi e sostituirli con NULL 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 al UPDATE 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.