Come UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) in PostgreSQL?


268

Una domanda molto frequente qui è come fare un upsert, che è ciò che MySQL chiama INSERT ... ON DUPLICATE UPDATEe lo standard supporta come parte MERGEdell'operazione.

Dato che PostgreSQL non lo supporta direttamente (prima di pag 9.5), come si fa? Considera quanto segue:

CREATE TABLE testtable (
    id integer PRIMARY KEY,
    somedata text NOT NULL
);

INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');

Ora immaginate che si desidera "upsert" le tuple (2, 'Joe'), (3, 'Alan'), in modo che i nuovi contenuti della tabella sarebbe:

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing tuple
(3, 'Alan')    -- Added new tuple

Questo è ciò di cui le persone parlano quando discutono di upsert. Fondamentalmente, qualsiasi approccio deve essere sicuro in presenza di più transazioni che lavorano sulla stessa tabella - utilizzando il blocco esplicito o difendendo in altro modo dalle condizioni di gara risultanti.

Questo argomento è ampiamente discusso in Inserisci, su aggiornamenti duplicati in PostgreSQL? , ma si tratta di alternative alla sintassi di MySQL ed è cresciuto nel tempo un bel po 'di dettagli non correlati. Sto lavorando a risposte definitive.

Queste tecniche sono utili anche per "inserisci se non esiste, altrimenti non fare nulla", ovvero "inserisci ... su chiave duplicata ignora".



8
@MichaelHampton l'obiettivo qui era quello di creare una versione definitiva che non fosse confusa da più risposte obsolete - e bloccata, quindi nessuno può farci nulla. Non sono d'accordo con il closevote.
Craig Ringer,

Perché, allora questo sarebbe presto obsoleto - e bloccato, quindi nessuno poteva farci niente.
Michael Hampton,

2
@MichaelHampton Se sei preoccupato, forse potresti contrassegnare quello a cui ti sei collegato e chiedere che sia sbloccato in modo che possa essere ripulito, quindi possiamo unirlo. Sono solo stanco di avere l'unico ovvio vicino- as-dup per upsert è un casino così confuso e sbagliato.
Craig Ringer,

1
Che domande e risposte non sono bloccate!
Michael Hampton,

Risposte:


396

9.5 e successivi:

PostgreSQL 9.5 e supporto più recente INSERT ... ON CONFLICT UPDATE(e ON CONFLICT DO NOTHING), ovvero upsert.

Confronto conON DUPLICATE KEY UPDATE .

Spiegazione veloce .

Per l'utilizzo, consultare il manuale , in particolare la clausola conflitti_azione nel diagramma della sintassi e il testo esplicativo .

A differenza delle soluzioni per 9.4 e precedenti fornite di seguito, questa funzione funziona con più righe in conflitto e non richiede il blocco esclusivo o un ciclo di tentativi.

Il commit che aggiunge la funzione è qui e la discussione sul suo sviluppo è qui .


Se hai 9.5 e non hai bisogno di essere retrocompatibile, puoi smettere di leggere ora .


9.4 e precedenti:

PostgreSQL non ha alcuna funzione UPSERT(o MERGE) integrata e farlo in modo efficiente di fronte a un uso simultaneo è molto difficile.

Questo articolo discute il problema con dettagli utili .

In generale è necessario scegliere tra due opzioni:

  • Operazioni individuali di inserimento / aggiornamento in un ciclo di tentativi; o
  • Blocco della tabella e unione in batch

Ciclo di tentativi per riga singola

L'uso di upsert di riga individuali in un ciclo di tentativi è l'opzione ragionevole se si desidera che molte connessioni provino contemporaneamente a eseguire inserimenti.

La documentazione di PostgreSQL contiene una procedura utile che ti permetterà di farlo in un ciclo all'interno del database . Protegge da aggiornamenti persi e inserisce gare, a differenza della maggior parte delle soluzioni ingenue. Funzionerà solo in READ COMMITTEDmodalità ed è sicuro solo se è l'unica cosa che fai nella transazione, però. La funzione non funzionerà correttamente se i trigger o i tasti univoci secondari causano violazioni univoche.

Questa strategia è molto inefficiente. Ogniqualvolta pratico, dovresti fare la fila per il lavoro e fare un upsert in blocco come descritto di seguito.

Molte tentate soluzioni a questo problema non prendono in considerazione i rollback, quindi si traducono in aggiornamenti incompleti. Due transazioni corrono l'una con l'altra; uno di loro è riuscito INSERT; l'altro riceve un errore chiave duplicato e fa UPDATEinvece un errore . I UPDATEblocchi in attesa del INSERTrollback o del commit. Quando esegue il rollback, la UPDATEcondizione di ricontrollo corrisponde a zero righe, quindi anche se i UPDATEcommit non ha effettivamente effettuato l'upert previsto. Devi controllare il conteggio delle righe dei risultati e riprovare dove necessario.

Alcune soluzioni tentate non riescono anche a considerare le gare SELECT. Se provi l'ovvio e semplice:

-- THIS IS WRONG. DO NOT COPY IT. It's an EXAMPLE.

BEGIN;

UPDATE testtable
SET somedata = 'blah'
WHERE id = 2;

-- Remember, this is WRONG. Do NOT COPY IT.

INSERT INTO testtable (id, somedata)
SELECT 2, 'blah'
WHERE NOT EXISTS (SELECT 1 FROM testtable WHERE testtable.id = 2);

COMMIT;

quindi quando due vengono eseguiti contemporaneamente ci sono diverse modalità di errore. Uno è il problema già discusso con un nuovo controllo di aggiornamento. Un altro è dove entrambi UPDATEallo stesso tempo, abbinando zero righe e continuando. Quindi entrambi fanno il EXISTStest, che accade prima del INSERT. Entrambi ottengono zero righe, quindi entrambi fanno INSERT. Uno fallisce con un errore chiave duplicato.

Questo è il motivo per cui è necessario un ciclo di riprovare. Potresti pensare che puoi prevenire errori di chiave duplicati o perdere gli aggiornamenti con SQL intelligente, ma non puoi. È necessario controllare il conteggio delle righe o gestire errori di chiave duplicati (a seconda dell'approccio scelto) e riprovare.

Per favore non creare la tua soluzione per questo. Come con l'accodamento dei messaggi, probabilmente è sbagliato.

Bulk upert con blocco

A volte si desidera eseguire un upsert in blocco, in cui si dispone di un nuovo set di dati che si desidera unire in un set di dati esistente precedente. Questo è di gran lunga più efficiente rispetto ai singoli aumenti di riga e dovrebbe essere preferito ogni volta che è pratico.

In questo caso, in genere si segue la seguente procedura:

  • CREATEun TEMPORARYtavolo

  • COPY o inserire in blocco i nuovi dati nella tabella temporanea

  • LOCKla tabella di destinazione IN EXCLUSIVE MODE. Ciò consente ad altre transazioni di SELECT, ma non apportare modifiche alla tabella.

  • Esegui uno UPDATE ... FROMdei record esistenti utilizzando i valori nella tabella temporanea;

  • Esegui una INSERTdelle righe che non esistono già nella tabella di destinazione;

  • COMMIT, rilasciando la serratura.

Ad esempio, per l'esempio fornito nella domanda, utilizzare il multi-valore INSERTper popolare la tabella temporanea:

BEGIN;

CREATE TEMPORARY TABLE newvals(id integer, somedata text);

INSERT INTO newvals(id, somedata) VALUES (2, 'Joe'), (3, 'Alan');

LOCK TABLE testtable IN EXCLUSIVE MODE;

UPDATE testtable
SET somedata = newvals.somedata
FROM newvals
WHERE newvals.id = testtable.id;

INSERT INTO testtable
SELECT newvals.id, newvals.somedata
FROM newvals
LEFT OUTER JOIN testtable ON (testtable.id = newvals.id)
WHERE testtable.id IS NULL;

COMMIT;

Lettura correlata

Che dire MERGE?

Lo standard SQL in MERGErealtà ha una semantica della concorrenza scarsamente definita e non è adatto per l'uperting senza prima bloccare una tabella.

È un'istruzione OLAP davvero utile per l'unione dei dati, ma in realtà non è una soluzione utile per upsert sicuri per la concorrenza. Ci sono molti consigli per le persone che usano altri DBMS da utilizzare MERGEper gli upsert, ma in realtà è sbagliato.

Altri DB:


Nel upsert in blocco, esiste un valore possibile nell'eliminazione da newvals anziché nel filtro INSERT? Ad esempio WITH upd AS (UPDATE ... RETURNING newvals.id) ELIMINA DA newvals UTILIZZANDO upd DOVE newvals.id = upd.id, seguito da un INSERT INTO testtable SELEZIONA * DA newvals? La mia idea con questo: invece di filtrare due volte in INSERT (per JOIN / WHERE e per il vincolo univoco), riutilizzare i risultati del controllo di esistenza dall'aggiornamento, che sono già nella RAM, e potrebbero essere molto più piccoli. Questa potrebbe essere una vittoria se poche righe abbinate e / o newvals sono molto più piccole di quelle testabili.
Gunnlaugur Briem,

1
Ci sono ancora problemi irrisolti e per gli altri fornitori non è chiaro cosa funzioni e cosa no. 1. La soluzione di loop Postgres come indicato non funziona nel caso di più chiavi univoche. 2. Anche la chiave duplicata su per mysql non funziona per più chiavi univoche. 3. Le altre soluzioni per MySQL, SQL Server e Oracle pubblicate sopra funzionano? Sono possibili eccezioni in questi casi e dobbiamo fare il giro?
dan del

@danb Questo riguarda solo PostgreSQL. Non esiste una soluzione cross-vendor. La soluzione per PostgreSQL non funziona per più righe, purtroppo è necessario eseguire una transazione per riga. Le "soluzioni" che utilizzano MERGEper SQL Server e Oracle sono errate e soggette a condizioni di competizione, come notato sopra. Dovrai esaminare ciascun DBMS in modo specifico per scoprire come gestirli, posso davvero offrire consigli solo su PostgreSQL. L'unico modo per eseguire un upsert multilivello sicuro su PostgreSQL sarà se il supporto per upsert nativo viene aggiunto al server principale.
Craig Ringer,

Anche per PostGresQL la soluzione non funziona nel caso in cui una tabella abbia più chiavi univoche (aggiornando solo una riga). In tal caso, è necessario specificare quale chiave viene aggiornata. Ad esempio, potrebbe esserci una soluzione cross-vendor che utilizza jdbc.
dan

2
Postgres ora supporta UPSERT - git.postgresql.org/gitweb/…
Chris
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.