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

Come includere le righe escluse in RETURNING from INSERT... ON CONFLICT

L'errore che ottieni:

ON CONFLICT Il comando DO UPDATE non può influenzare la riga una seconda volta

indica che stai tentando di inserire la stessa riga più di una volta in un singolo comando. In altre parole:hai dei duplicati su (name, url, email) nel tuo VALUES elenco. Piega i duplicati (se questa è un'opzione) e dovrebbe funzionare. Ma dovrai decidere quale riga scegliere da ciascuna serie di duplicati.

INSERT INTO feeds_person (created, modified, name, url, email)
SELECT DISTINCT ON (name, url, email) *
FROM  (
   VALUES
   ('blah', 'blah', 'blah', 'blah', 'blah')
   -- ... more
   ) v(created, modified, name, url, email)  -- match column list
ON     CONFLICT (name, url, email) DO UPDATE
SET    url = feeds_person.url
RETURNING id;

Poiché utilizziamo un VALUES indipendente expression ora, devi aggiungere cast di tipi espliciti per tipi non predefiniti. Come:

VALUES
    (timestamptz '2016-03-12 02:47:56+01'
   , timestamptz '2016-03-12 02:47:56+01'
   , 'n3', 'u3', 'e3')
   ...

Il tuo timestamptz le colonne richiedono un cast di tipo esplicito, mentre i tipi di stringa possono funzionare con text predefinito . (Puoi ancora trasmettere a varchar(n) subito.)

Ci sono modi per determinare quale riga scegliere da ciascuna serie di duplicati:

  • Seleziona la prima riga in ogni gruppo GROUP BY?

Hai ragione, non c'è (attualmente) alcun modo per essere esclusi righe nel RETURNING clausola. Cito il Wiki di Postgres:

Nota che RETURNING non rende visibile il "EXCLUDED.* " aliasdal UPDATE (solo il generico "TARGET.* " l'alias è visibile lì). Si ritiene che ciò crei una fastidiosa ambiguità per questi casi semplici e comuni [30] con scarsi o nessun beneficio. Ad un certo punto in futuro, potremmo perseguire un modo per esporre seRETURNING -le tuple proiettate sono state inserite e aggiornate, ma questo probabilmente non ha bisogno di essere inserito nella prima iterazione commit della funzione [31].

Tuttavia , non dovresti aggiornare le righe che non dovrebbero essere aggiornate. Gli aggiornamenti vuoti sono costosi quasi quanto gli aggiornamenti regolari e potrebbero avere effetti collaterali indesiderati. Non hai strettamente bisogno di UPSERT per cominciare, il tuo caso assomiglia più a "SELECT o INSERT". Correlati:

  • SELEZIONA o INSERT in una funzione soggetta a condizioni di gara?

Uno un modo più pulito per inserire un set di righe sarebbe con CTE che modificano i dati:

WITH val AS (
   SELECT DISTINCT ON (name, url, email) *
   FROM  (
      VALUES 
      (timestamptz '2016-1-1 0:0+1', timestamptz '2016-1-1 0:0+1', 'n', 'u', 'e')
    , ('2016-03-12 02:47:56+01', '2016-03-12 02:47:56+01', 'n1', 'u3', 'e3')
      -- more (type cast only needed in 1st row)
      ) v(created, modified, name, url, email)
   )
, ins AS (
   INSERT INTO feeds_person (created, modified, name, url, email)
   SELECT created, modified, name, url, email FROM val
   ON     CONFLICT (name, url, email) DO NOTHING
   RETURNING id, name, url, email
   )
SELECT 'inserted' AS how, id FROM ins  -- inserted
UNION  ALL
SELECT 'selected' AS how, f.id         -- not inserted
FROM   val v
JOIN   feeds_person f USING (name, url, email);

La complessità aggiunta dovrebbe pagare per grandi tabelle in cui INSERT è la regola e SELECT l'eccezione.

Inizialmente avevo aggiunto un NOT EXISTS predicato sull'ultimo SELECT per evitare duplicati nel risultato. Ma era superfluo. Tutti i CTE di una singola query visualizzano gli stessi snapshot di tabelle. Il set restituito con ON CONFLICT (name, url, email) DO NOTHING si esclude a vicenda per il set restituito dopo il INNER JOIN sulle stesse colonne.

Sfortunatamente questo apre anche una piccola finestra per una condizione di gara . Se...

  • una transazione simultanea inserisce righe in conflitto
  • non si è ancora impegnato
  • ma alla fine si impegna

... alcune righe potrebbero andare perse.

Potresti semplicemente INSERT .. ON CONFLICT DO NOTHING , seguito da un separato SELECT query per tutte le righe - all'interno della stessa transazione per superare questo problema. Che a sua volta apre un'altra piccola finestra per una condizione di gara se le transazioni simultanee possono eseguire il commit di scritture nella tabella tra INSERT e SELECT (in default READ COMMITTED livello di isolamento). Può essere evitato con REPEATABLE READ isolamento della transazione (o più rigoroso). O con un blocco di scrittura (possibilmente costoso o addirittura inaccettabile) sull'intero tavolo. Puoi ottenere qualsiasi comportamento di cui hai bisogno, ma potrebbe esserci un prezzo da pagare.

Correlati:

  • Come utilizzare RETURNING con ON CONFLICT in PostgreSQL?
  • Restituisci le righe da INSERT con ON CONFLICT senza dover aggiornare