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

Come utilizzare RETURNING con ON CONFLICT in PostgreSQL?

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 .