Perché le righe inserite in un CTE non possono essere aggiornate nella stessa istruzione?


12

In PostgreSQL 9.5, data una semplice tabella creata con:

create table tbl (
    id serial primary key,
    val integer
);

Corro SQL per INSERIRE un valore, quindi AGGIORNA nella stessa istruzione:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Il risultato è che l'AGGIORNAMENTO viene ignorato:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

Perchè è questo? Questa limitazione fa parte dello standard SQL (ovvero è presente in altri database) o qualcosa di specifico in PostgreSQL che potrebbe essere risolto in futuro? La documentazione sulle query WITH indica che più UPDATE non sono supportati, ma non menziona INSERT e UPDATE.

Risposte:


14

Tutte le dichiarazioni in un CTE avvengono praticamente contemporaneamente. Cioè, si basano sulla stessa istantanea del database.

The UPDATEvede lo stesso stato della tabella sottostante come INSERT, il che significa che la riga con val = 1non è ancora presente. Il manuale chiarisce qui:

Tutte le istruzioni vengono eseguite con la stessa istantanea (vedere il capitolo 13 ), quindi non possono "vedere" gli effetti reciproci sulle tabelle di destinazione.

Ogni istruzione può vedere cosa viene restituito da un altro CTE nella RETURNINGclausola. Ma le tabelle sottostanti sembrano uguali a loro.

Avresti bisogno di due dichiarazioni (in una singola transazione) per quello che stai cercando di fare. L'esempio dato dovrebbe davvero essere solo uno INSERTper cominciare, ma ciò potrebbe essere dovuto all'esempio semplificato.


14

Questa è una decisione di implementazione. È descritto nella documentazione di Postgres, WITHQuery (espressioni comuni di tabelle) . Esistono due paragrafi relativi al problema.

Innanzitutto, la ragione del comportamento osservato:

Le istruzioni secondarie in WITHvengono eseguite simultaneamente tra loro e con la query principale . Pertanto, quando si utilizzano le istruzioni di modifica dei dati in WITH, l'ordine in cui si verificano effettivamente gli aggiornamenti specificati è imprevedibile. Tutte le istruzioni vengono eseguite con la stessa istantanea (vedere il capitolo 13), quindi non possono "vedere" gli effetti reciproci sulle tabelle di destinazione. Ciò allevia gli effetti dell'imprevedibilità dell'ordine effettivo degli aggiornamenti delle righe e indica che i RETURNINGdati sono l'unico modo per comunicare i cambiamenti tra le diverse WITHdichiarazioni secondarie e la query principale. Un esempio di questo è che in ...

Dopo aver pubblicato un suggerimento insieme a pgsql-docs , Marko Tiikkaja ha spiegato (che concorda con la risposta di Erwin):

I casi insert-update e insert-delete non funzionano perché UPDATE e DELETE non hanno modo di vedere le righe INSERTed a causa della loro istantanea che è stata eseguita prima che INSERT avvenisse. Non c'è nulla di imprevedibile in questi due casi.

Quindi il motivo per cui la tua dichiarazione non si aggiorna può essere spiegato dal primo paragrafo sopra (sulle "istantanee"). Ciò che accade quando si modificano i CTE è che tutti e la query principale vengono eseguiti e "vedono" la stessa istantanea dei dati (tabelle), come erano immediatamente prima dell'esecuzione dell'istruzione. I CTE possono trasmettere informazioni su ciò che hanno inserito / aggiornato / cancellato tra loro e alla query principale utilizzando la RETURNINGclausola ma non possono vedere direttamente le modifiche nelle tabelle. Quindi vediamo cosa succede nella tua dichiarazione:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Abbiamo 2 parti, il CTE ( newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

e la query principale:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

Il flusso di esecuzione è qualcosa del genere:

           initial data: tbl
                id  val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id  val           \
            1    1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

Di conseguenza, quando la query principale unisce tbl(come mostrato nell'istantanea) alla newvaltabella, unisce una tabella vuota con una tabella a 1 riga. Ovviamente aggiorna 0 righe. Quindi l'istruzione non è mai arrivata per modificare la riga appena inserita ed è quello che vedi.

La soluzione nel tuo caso è riscrivere l'istruzione per inserire i valori corretti in primo luogo o utilizzare 2 istruzioni. Uno che inserisce e un secondo da aggiornare.


Esistono altre situazioni simili, come se l'istruzione avesse una INSERTe poi una DELETEsulle stesse righe. La cancellazione fallirebbe esattamente per gli stessi motivi.

Alcuni altri casi, con update-update e update-delete e il loro comportamento sono spiegati in un paragrafo seguente, nella stessa pagina dei documenti.

Il tentativo di aggiornare la stessa riga due volte in una singola istruzione non è supportato. Viene eseguita solo una delle modifiche, ma non è facile (e talvolta non è possibile) prevedere in modo affidabile quale. Questo vale anche per l'eliminazione di una riga che era già stata aggiornata nella stessa istruzione: viene eseguito solo l'aggiornamento. Pertanto, dovresti generalmente evitare di provare a modificare una singola riga due volte in una singola istruzione. In particolare, evitare di scrivere dichiarazioni secondarie WITH che potrebbero influire sulle stesse righe modificate dall'istruzione principale o da una istruzione secondaria di pari livello. Gli effetti di tale affermazione non saranno prevedibili.

E nella risposta di Marko Tiikkaja:

I casi update-update e update-delete non sono esplicitamente causati dallo stesso dettaglio dell'implementazione sottostante (come i casi insert-update e insert-delete).
Il caso update-update non funziona perché assomiglia internamente al problema di Halloween e Postgres non ha modo di sapere quali tuple andrebbero bene aggiornare due volte e quali potrebbero reintrodurre il problema di Halloween.

Quindi la ragione è la stessa (come vengono implementati i CTE di modifica e come ogni CTE vede la stessa istantanea) ma i dettagli differiscono in questi 2 casi, in quanto più complessi e i risultati possono essere imprevedibili nel caso aggiornamento-aggiornamento.

Nell'insert-update (come nel tuo caso) e in un simile insert-delete i risultati sono prevedibili. Solo l'inserimento avviene poiché la seconda operazione (aggiorna o elimina) non ha modo di vedere e influire sulle righe appena inserite.


La soluzione suggerita è la stessa per tutti i casi che tentano di modificare le stesse righe più di una volta: non farlo. Scrivi istruzioni che modificano ogni riga una volta o usa istruzioni separate (2 o più).

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.