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 UPDATE
riga, 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, VACUUM
costo. 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 chats
funziona 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' VALUES
espressione è 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 VALUES
viene 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 COMMITTED
l'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 INSERT
rileverà un conflitto (l' UNIQUE
indice / vincolo è assoluto) e DO NOTHING
quindi non restituirà la riga. (Inoltre, non è possibile bloccare la riga come dimostrato nel problema di concorrenza 2 di seguito, poiché non è visibile .) Viene visualizzata SELECT
la 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 SELECT
likeFOR 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 VALUES
nell'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 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. 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 chats
nell'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
.
ON CONFLICT UPDATE
modo che vi sia una modifica alla riga. QuindiRETURNING
lo catturerà.