La risposta attualmente accettata sembra ok per un singolo obiettivo di conflitto, pochi conflitti, piccole tuple e nessun trigger. Evita il problema di concorrenza 1 (vedi sotto) con la forza bruta. La soluzione semplice ha il suo fascino, gli effetti collaterali possono essere meno importanti.
Per tutti gli altri casi, tuttavia, non aggiornare righe identiche senza bisogno. Anche se non vedi alcuna differenza in superficie, ci sono vari effetti collaterali :
-
Potrebbe attivare trigger che non dovrebbero essere attivati.
-
Blocca in scrittura le righe "innocenti", con possibili costi per transazioni simultanee.
-
Potrebbe far sembrare nuova la riga, sebbene sia vecchia (marcatura temporale della transazione).
-
La cosa più importante , con il modello MVCC di PostgreSQL viene scritta una nuova versione di riga per ogni
UPDATE
, indipendentemente dal fatto che i dati della riga siano cambiati. Ciò comporta una penalizzazione delle prestazioni per l'UPSERT stesso, rigonfiamento del tavolo, rigonfiamento dell'indice, penalità delle prestazioni per le successive operazioni sul tavolo,VACUUM
costo. Un effetto minore per pochi duplicati, ma massiccio per lo più duplicati.
Più , a volte non è pratico o addirittura possibile usare ON CONFLICT DO UPDATE
. Il manuale:
Per ON CONFLICT DO UPDATE
, un conflict_target
deve essere fornito.
Un single "obiettivo di conflitto" non è possibile se sono coinvolti più indici/vincoli. Ma ecco una soluzione correlata per più indici parziali:
- UPSERT basato sul vincolo UNIQUE con valori NULL
Tornando sull'argomento, puoi ottenere (quasi) lo stesso senza aggiornamenti vuoti ed effetti collaterali. Alcune delle seguenti soluzioni funzionano anche con ON CONFLICT DO NOTHING
(nessun "bersaglio in conflitto"), per catturare tutti possibili conflitti che potrebbero sorgere, che potrebbero essere o meno 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. Potrebbe essere necessario per distinguere i due casi (un altro vantaggio rispetto alle scritture vuote).
L'ultima JOIN chats
funziona perché le righe appena inserite da un CTE di modifica dei dati allegato non sono ancora visibili nella tabella sottostante. (Tutte le parti della stessa istruzione SQL vedono gli stessi snapshot delle tabelle sottostanti.)
Dal momento che i VALUES
l'espressione è indipendente (non direttamente collegata a un INSERT
) Postgres non può derivare tipi di dati dalle colonne di destinazione e potrebbe essere necessario aggiungere cast di tipi espliciti. Il manuale:
Quando VALUES
è usato in INSERT
, i valori vengono tutti automaticamente forzati al 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 tutte.
La query stessa (senza contare gli effetti collaterali) potrebbe essere un po' più costosa per pochi duplicati, a causa dell'overhead del CTE e dell'ulteriore SELECT
(che dovrebbe essere economico poiché l'indice perfetto esiste per definizione:un vincolo univoco viene implementato con un indice).
Potrebbe essere (molto) più veloce per molti duplicati. Il costo effettivo delle scritture aggiuntive dipende da molti fattori.
Ma ci sono meno effetti collaterali e costi nascosti comunque. Molto probabilmente è più economico nel complesso.
Le sequenze allegate sono ancora avanzate, poiché i valori predefiniti vengono compilati prima test per i conflitti.
Informazioni sui CTE:
- Le query di tipo SELECT sono l'unico tipo che può essere nidificato?
- Deduplica le istruzioni SELECT nella divisione relazionale
Con carico di scrittura simultaneo
Presupponendo predefinito READ COMMITTED
isolamento delle transazioni. Correlati:
- Le transazioni simultanee risultano in race condition con vincolo univoco sull'inserimento
La migliore strategia per difendersi dalle condizioni di gara dipende dai requisiti esatti, dal numero e dalla dimensione delle righe 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 è stata scritta in una riga che la tua transazione ora tenta di UPSERT, la tua transazione deve attendere che l'altra finisca.
Se l'altra transazione termina con ROLLBACK
(o qualsiasi errore, ovvero ROLLBACK
automatico ), la transazione può procedere normalmente. Possibile effetto collaterale minore:lacune nei numeri sequenziali. Ma nessuna riga mancante.
Se l'altra transazione termina normalmente (COMMIT
implicito o esplicito ), il tuo INSERT
rileverà un conflitto (il UNIQUE
index/vincolo è assoluto) e DO NOTHING
, quindi anche non restituisce la riga. (Inoltre, non è possibile bloccare la riga come illustrato in problema di concorrenza 2 di seguito, poiché non è visibile .) Il SELECT
vede lo stesso snapshot dall'inizio della query e inoltre non può restituire la riga ancora invisibile.
Qualsiasi riga di questo tipo manca nel set di risultati (anche se esistono nella tabella sottostante)!
Questo potrebbe andare bene 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. Potrebbe essere abbastanza buono per il caso raro. Il punto è avviare una nuova query (può essere nella stessa transazione), che vedrà quindi le righe appena salvate.
Oppure controlla le righe dei risultati mancanti all'interno la stessa query e sovrascrivere quelli 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 completo insieme di risultati. L'ultimo CTE non farà nulla per la maggior parte del tempo. Solo se le righe risultano mancanti dal risultato restituito, utilizziamo la forza bruta.
Più spese generali, ancora. Più sono i conflitti con le righe preesistenti, più è probabile che superi l'approccio semplice.
Un effetto collaterale:il 2° UPSERT scrive le righe fuori ordine, quindi reintroduce la possibilità di deadlock (vedi sotto) se tre o più le 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 coinvolte delle righe interessate e devi assicurarti che le righe che hai trovato siano ancora presenti in una fase successiva della stessa transazione, puoi bloccare le righe esistenti a buon mercato nel CTE ins
(che altrimenti verrebbe sbloccato) con:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
E aggiungi una clausola di blocco a SELECT
anche, come FOR UPDATE
.
Ciò fa sì che le operazioni di scrittura concorrenti attendano fino alla fine della transazione, quando tutti i blocchi vengono rilasciati. Quindi sii breve.
Maggiori dettagli e spiegazione:
- Come includere le righe escluse in RETURNING from INSERT ... ON CONFLICT
- SELEZIONA o INSERT in una funzione soggetta a condizioni di gara?
Deadlock?
Difenditi dagli stalli di stallo inserendo le righe in ordine coerente . Vedi:
- Deadlock con INSERT a più righe nonostante IN CONFLITTO NON FARE NIENTE
Tipi di dati e cast
Tabella esistente come modello per i tipi di dati ...
Cast di tipo esplicito per la prima riga di dati nel VALUES
indipendente l'espressione può essere scomoda. Ci sono modi per aggirarlo. È possibile utilizzare qualsiasi relazione esistente (tabella, vista, ...) come modello di riga. La tabella di destinazione è la scelta più ovvia per il caso d'uso. I dati di input vengono forzati automaticamente ai tipi appropriati, come in VALUES
clausola 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. Vedi:
- Trasmissione del tipo NULL durante l'aggiornamento di più righe
... e nomi
Funziona anche per tutti tipi di dati.
Durante l'inserimento in tutte le colonne (iniziali) della tabella, puoi omettere i nomi delle colonne. Supponendo che la tabella chats
nell'esempio è costituito solo dalle 3 colonne utilizzate nell'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 parole riservate come "user"
come identificatore. Quella è una pistola carica. Utilizzare identificatori legali, minuscoli e senza virgolette. L'ho sostituito con usr
.