Come usare RETURNING con ON CONFLICT in PostgreSQL?


149

Ho il seguente UPSERT in PostgreSQL 9.5:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

Se non ci sono conflitti restituisce qualcosa del genere:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Ma se ci sono conflitti non restituisce alcuna riga:

----------
    | id |
----------

Voglio restituire le nuove idcolonne se non ci sono conflitti o restituire le idcolonne esistenti delle colonne in conflitto.
Può essere fatto? Se sì, come?


1
Utilizzare in ON CONFLICT UPDATEmodo che vi sia una modifica alla riga. Quindi RETURNINGlo catturerà.
Gordon Linoff,

1
@GordonLinoff Cosa succede se non c'è nulla da aggiornare?
Okku,

1
Se non c'è nulla da aggiornare, significa che non c'è stato alcun conflitto, quindi inserisce solo i nuovi valori e restituisce il loro id
zola

1
Troverai altri modi qui . Mi piacerebbe sapere la differenza tra i due in termini di prestazioni.
Stanislasdrg Ripristina Monica il

Risposte:


88

Ho avuto esattamente lo stesso problema e l'ho risolto usando "do update" anziché "do nothing", anche se non avevo nulla da aggiornare. Nel tuo caso sarebbe qualcosa del genere:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Questa query restituirà tutte le righe, indipendentemente dal fatto che siano state appena inserite o esistessero prima.


11
Un problema con questo approccio è che il numero di sequenza della chiave primaria viene incrementato ad ogni conflitto (aggiornamento fasullo), il che significa sostanzialmente che potresti finire con enormi lacune nella sequenza. Qualche idea su come evitarlo?
Mischa,

9
@Mischa: e allora? Le sequenze non sono mai garantite per essere gapless in primo luogo e le lacune non contano (e se lo fanno, una sequenza è la cosa sbagliata da fare)
a_horse_with_no_name

24
Vorrei non consiglierei di utilizzare questo in molti casi. Ho aggiunto una risposta perché.
Erwin Brandstetter

4
Questa risposta non sembra raggiungere l' DO NOTHINGaspetto della domanda originale - per me sembra aggiornare il campo non in conflitto (qui, "nome") per tutte le righe.
PeterJCLaw,

Come discusso nella lunghissima risposta di seguito, l'uso di "Do Update" per un campo che non è stato modificato non è una soluzione "pulita" e può causare altri problemi.
Bill Worthington,

202

La risposta attualmente accettata sembra ok per un singolo target di conflitto, pochi conflitti, piccole tuple e nessun trigger. Evita il problema di concorrenza 1 (vedi sotto) con la forza bruta. La semplice soluzione ha il suo fascino, gli effetti collaterali possono essere meno importanti.

Per tutti gli altri casi, comunque non aggiornare le righe identiche senza necessità. Anche se non vedi alcuna differenza sulla superficie, ci sono vari effetti collaterali :

  • Potrebbe innescare trigger che non dovrebbero essere attivati.

  • Blocca le righe "innocenti", eventualmente sostenendo costi per transazioni simultanee.

  • Potrebbe rendere la riga nuova, sebbene sia vecchia (data e ora della transazione).

  • Soprattutto , con il modello MVCC di PostgreSQL viene scritta una nuova versione per ogni UPDATEriga, indipendentemente dal fatto che i dati della riga siano cambiati. Ciò comporta una penalità di prestazione per UPSERT stesso, rigonfiamento della tabella, rigonfiamento dell'indice, penalità di prestazione per le successive operazioni sulla tabella, VACUUMcosto. Un effetto secondario per pochi duplicati, ma enorme per la maggior parte dei .

Inoltre , a volte non è pratico o addirittura possibile utilizzareON CONFLICT DO UPDATE . Il manuale:

Per ON CONFLICT DO UPDATE, aconflict_target deve essere fornito.

UN singolo "target di conflitto" non è possibile se sono coinvolti più indici / vincoli.

Puoi ottenere (quasi) lo stesso senza aggiornamenti vuoti ed effetti collaterali. Alcune delle seguenti soluzioni funzionano anche conON CONFLICT DO NOTHING (nessun "obiettivo di conflitto"), per cogliere tutti i possibili conflitti che potrebbero sorgere - che possono o meno essere desiderabili.

Senza carico di scrittura simultaneo

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, ins AS (
   INSERT INTO chats (usr, contact, name) 
   SELECT * FROM input_rows
   ON CONFLICT (usr, contact) DO NOTHING
   RETURNING id  --, usr, contact              -- return more columns?
   )
SELECT 'i' AS source                           -- 'i' for 'inserted'
     , id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
     , c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

Il source colonna è un'aggiunta facoltativa per dimostrare come funziona. In realtà potresti averne bisogno per dire la differenza tra i due casi (un altro vantaggio rispetto alle scritture vuote).

Il finale JOIN chatsfunziona perché le righe appena inserite da un allegato CTE che modifica i dati non sono ancora visibili nella tabella sottostante. (Tutte le parti della stessa istruzione SQL vedono le stesse istantanee delle tabelle sottostanti.)

Poiché l' VALUESespressione è indipendente (non direttamente collegata a INSERT) Postgres non può derivare tipi di dati dalle colonne di destinazione e potrebbe essere necessario aggiungere cast di tipi espliciti.Il manuale:

Quando VALUESviene utilizzato inINSERT , i valori vengono automaticamente forzati sul tipo di dati della colonna di destinazione corrispondente. Quando viene utilizzato in altri contesti, potrebbe essere necessario specificare il tipo di dati corretto. Se le voci sono tutte costanti letterali tra virgolette, è sufficiente forzare la prima per determinare il tipo assunto per tutti.

La query stessa (senza contare gli effetti collaterali) potrebbe essere un po 'più costosa per pochi duplicati, a causa del sovraccarico del CTE e dell'ulteriore SELECT(che dovrebbe essere economico poiché l'indice perfetto è lì per definizione - un vincolo unico è implementato con un indice).

Potrebbe essere (molto) più veloce per molti duplicati. Il costo effettivo di ulteriori scritture dipende da molti fattori.

Ma ci sono meno effetti collaterali e costi nascosti in ogni caso. È molto probabilmente più economico nel complesso.

Le sequenze allegate sono ancora avanzate, poiché i valori predefiniti sono stati inseriti in precedenza verificare i conflitti.

Informazioni sui CTE:

Con carico di scrittura simultaneo

Supponendo READ COMMITTEDl'isolamento di transazione predefinito . Relazionato:

La migliore strategia per difendersi dalle condizioni di gara dipende dai requisiti esatti, dal numero e dalle dimensioni delle file nella tabella e negli UPSERT, dal numero di transazioni simultanee, dalla probabilità di conflitti, dalle risorse disponibili e da altri fattori ...

Problema di concorrenza 1

Se una transazione simultanea ha scritto in una riga che la transazione tenta ora di UPSERT, la transazione deve attendere il completamento dell'altra.

Se l'altra transazione termina con ROLLBACK(o qualsiasi errore, ovvero automatico ROLLBACK), la transazione può procedere normalmente. Possibili effetti collaterali minori: lacune nei numeri sequenziali. Ma nessuna riga mancante.

Se l'altra transazione termina normalmente (implicita o esplicita COMMIT), l'utente INSERTrileverà un conflitto (l' UNIQUEindice / vincolo è assoluto) e DO NOTHINGquindi non restituirà la riga. (Inoltre, non è possibile bloccare la riga come dimostrato nel problema di concorrenza 2 di seguito, poiché non è visibile .) Viene visualizzata SELECTla stessa istantanea dall'inizio della query e non è possibile restituire la riga ancora invisibile.

Qualsiasi riga del genere non è presente nel set di risultati (anche se esiste nella tabella sottostante)!

Questo potrebbe essere ok così com'è . Soprattutto se non stai restituendo righe come nell'esempio e sei soddisfatto sapendo che la riga è lì. Se questo non è abbastanza buono, ci sono vari modi per aggirarlo.

È possibile controllare il conteggio delle righe dell'output e ripetere l'istruzione se non corrisponde al conteggio delle righe dell'input. Può essere abbastanza buono per il raro caso. Il punto è avviare una nuova query (può trovarsi nella stessa transazione), che vedrà quindi le righe appena impegnate.

Oppure controlla le righe dei risultati mancanti nella stessa query e sovrascrivi quelle con il trucco della forza bruta dimostrato nella risposta di Alextoni .

WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
   INSERT INTO chats AS c (usr, contact, name) 
   SELECT * FROM input_rows
   ON     CONFLICT (usr, contact) DO NOTHING
   RETURNING id, usr, contact                   -- we need unique columns for later join
   )
, sel AS (
   SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
        , id, usr, contact
   FROM   ins
   UNION  ALL
   SELECT 's'::"char" AS source                 -- 's' for 'selected'
        , c.id, usr, contact
   FROM   input_rows
   JOIN   chats c USING (usr, contact)
   )
, ups AS (                                      -- RARE corner case
   INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
   SELECT i.*
   FROM   input_rows i
   LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
   WHERE  s.usr IS NULL                         -- missing!
   ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
   SET    name = c.name                         -- ... this time we overwrite with old value
   -- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
   RETURNING 'u'::"char" AS source              -- 'u' for updated
           , id  --, usr, contact               -- return more columns?
   )
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

È come la query sopra, ma aggiungiamo un altro passaggio con il CTE ups, prima di restituire il set di risultati completo . L'ultimo CTE non farà nulla per la maggior parte del tempo. Solo se le righe mancano dal risultato restituito, usiamo la forza bruta.

Più sovraccarico, ancora. Maggiore è il conflitto con righe preesistenti, maggiore è la probabilità che questo superi le prestazioni del semplice approccio.

Un effetto collaterale: il 2 ° UPSERT scrive le righe fuori servizio, quindi reintroduce la possibilità di deadlock (vedi sotto) se tre o più transazioni che scrivono sulle stesse righe si sovrappongono. Se questo è un problema, hai bisogno di una soluzione diversa, come ripetere l'intera affermazione come menzionato sopra.

Problema di concorrenza 2

Se le transazioni simultanee possono scrivere nelle colonne interessate delle righe interessate e devi assicurarti che le righe che hai trovato siano ancora lì in una fase successiva della stessa transazione, puoi bloccare le righe esistenti a buon mercato nel CTE ins(che altrimenti verrebbero sbloccate) con:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

E aggiungi anche una clausola di blocco al SELECTlikeFOR UPDATE .

Ciò rende le operazioni di scrittura concorrenti in attesa fino alla fine della transazione, quando tutti i blocchi vengono rilasciati. Quindi sii breve.

Maggiori dettagli e spiegazioni:

Deadlock?

Difendersi dai deadlock inserendo le righe in ordine coerente . Vedere:

Tipi di dati e cast

Tabella esistente come modello per i tipi di dati ...

Il cast di tipi espliciti per la prima riga di dati VALUESnell'espressione indipendente può essere scomodo. Ci sono modi per aggirarlo. È possibile utilizzare qualsiasi relazione esistente (tabella, vista, ...) come modello di riga. La tabella di destinazione è la scelta ovvia per il caso d'uso. I dati di input vengono forzati automaticamente nei tipi appropriati, come nella VALUESclausola di un INSERT:

WITH input_rows AS (
  (SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo1', 'bar1', 'bob1')  -- no type casts here
    , ('foo2', 'bar2', 'bob2')
   )
   ...

Questo non funziona per alcuni tipi di dati. Vedere:

... e nomi

Questo funziona anche per tutti i tipi di dati.

Durante l'inserimento in tutte le colonne (iniziali) della tabella, è possibile omettere i nomi delle colonne. Supponendo che la tabella chatsnell'esempio consista solo delle 3 colonne utilizzate in UPSERT:

WITH input_rows AS (
   SELECT * FROM (
      VALUES
      ((NULL::chats).*)         -- copies whole row definition
      ('foo1', 'bar1', 'bob1')  -- no type casts needed
    , ('foo2', 'bar2', 'bob2')
      ) sub
   OFFSET 1
   )
   ...

A parte: non usare le parole riservate come "user"come identificatore. È una pistola carica. Utilizzare identificatori legali, minuscoli, non quotati. L'ho sostituito con usr.


2
Implichi che questo metodo non creerà lacune nei seriali, ma sono: INSERT ... ON CONFLICT DO NOTING non incrementa il serial ogni volta da ciò che posso vedere
dannoso

1
non è così importante, ma perché i periodici vengono incrementati? e non c'è modo di evitarlo?
saliente

1
@salient: Come ho aggiunto sopra: i valori predefiniti della colonna vengono compilati prima che i test per i conflitti e le sequenze non vengano mai ripristinati, per evitare conflitti con scritture simultanee.
Erwin Brandstetter,

7
Incredibile. Funziona come un fascino e facile da capire una volta che lo guardi attentamente. Vorrei ancora ON CONFLICT SELECT...dove una cosa però :)
Roshambo

3
Incredibile. I creatori di Postgres sembrano torturare gli utenti. Perché non basta semplicemente fare ritorno clausola restituisce sempre valori, indipendentemente dal fatto che ci sono stati inserti o no?
Anatoly Alekseev,

16

Upsert, essendo un'estensione della INSERTquery può essere definito con due comportamenti diversi in caso di conflitto di vincolo: DO NOTHINGo DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Nota anche che RETURNINGnon restituisce nulla, perché non sono state inserite tuple . Ora con DO UPDATE, è possibile eseguire operazioni sulla tupla con cui c'è un conflitto. Innanzitutto, è importante definire un vincolo che verrà utilizzato per definire l'esistenza di un conflitto.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
Bel modo di ottenere sempre l'ID riga interessato e sapere se si trattava di un inserimento o di un aggiornamento. Proprio quello di cui avevo bisogno.
Moby Duck,

Questo sta ancora usando il "Do Update", di cui sono già stati discussi gli svantaggi.
Bill Worthington,

4

Per gli inserimenti di un singolo articolo, probabilmente userei una coalescenza quando restituisco l'id:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

Lo scopo principale dell'utilizzo ON CONFLICT DO NOTHINGè evitare errori di lancio, ma non causerà la restituzione di righe. Quindi abbiamo bisogno di un altro SELECTper ottenere l'ID esistente.

In questo SQL, se fallisce nei conflitti, non restituirà nulla, quindi il secondo SELECTotterrà la riga esistente; se si inserisce correttamente, allora ci saranno due stessi record, quindi dobbiamo UNIONunire il risultato.


Questa soluzione funziona bene ed evita di fare una scrittura (aggiornamento) non necessaria sul DB !! Bello!
Simon C

0

Ho modificato la straordinaria risposta di Erwin Brandstetter, che non aumenterà la sequenza e non bloccherà la scrittura di alcuna riga. Sono relativamente nuovo su PostgreSQL, quindi non esitare a farmi sapere se riscontri degli svantaggi di questo metodo:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

Ciò presuppone che la tabella chatsabbia un vincolo univoco per le colonne(usr, contact) .

Aggiornamento: aggiunte le revisioni suggerite dallo spatar (sotto). Grazie!


1
Invece di CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existsscrivere r.id IS NOT NULL as row_exists. Invece di WHERE row_exists=FALSEscrivere WHERE NOT row_exists.
spatar,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.