Pourquoi les lignes insérées dans un CTE ne peuvent-elles pas être mises à jour dans la même instruction?

13

Dans PostgreSQL 9.5, étant donné une table simple créée avec:

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

J'exécute SQL pour INSÉRER une valeur, puis MISE À JOUR dans la même instruction:

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

Le résultat est que la MISE À JOUR est ignorée:

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

Pourquoi est-ce? Cette limitation fait-elle partie du standard SQL (c'est-à-dire présente dans d'autres bases de données), ou quelque chose de spécifique à PostgreSQL qui pourrait être corrigé à l'avenir? La documentation des requêtes WITH indique que plusieurs mises à jour ne sont pas prises en charge, mais ne mentionne pas les insertions et les mises à jour.

Jeff Turner
la source

Réponses:

15

Toutes les déclarations dans un CTE se produisent pratiquement en même temps. C'est-à-dire, ils sont basés sur le même instantané de la base de données.

Le UPDATEvoit le même état de la table sous-jacente que le INSERT, ce qui signifie que la ligne avec val = 1n'est pas encore là. Le manuel précise ici:

Toutes les instructions sont exécutées avec le même instantané (voir le chapitre 13 ), elles ne peuvent donc pas "voir" les effets des autres sur les tables cibles.

Chaque instruction peut voir ce qui est retourné par un autre CTE dans la RETURNINGclause. Mais les tables sous-jacentes leur sont identiques.

Vous auriez besoin de deux déclarations (en une seule transaction) pour ce que vous essayez de faire. L'exemple donné ne devrait vraiment être qu'un simple INSERTpour commencer, mais cela peut être dû à l'exemple simplifié.

Erwin Brandstetter
la source
15

Il s'agit d'une décision de mise en œuvre. Il est décrit dans la documentation Postgres, WITHRequêtes (expressions de table communes) . Il y a deux paragraphes liés à la question.

Tout d'abord, la raison du comportement observé:

Les sous-instructions de WITHsont exécutées simultanément entre elles et avec la requête principale . Par conséquent, lorsque vous utilisez des instructions de modification de données dans WITH, l'ordre dans lequel les mises à jour spécifiées se produisent réellement est imprévisible. Toutes les instructions sont exécutées avec le même instantané (voir le chapitre 13), elles ne peuvent donc pas "voir" les effets des autres sur les tables cibles. Cela atténue les effets de l'imprévisibilité de l'ordre réel des mises à jour des lignes et signifie que les RETURNINGdonnées sont le seul moyen de communiquer les modifications entre les différentes WITHsous-instructions et la requête principale. Un exemple de ceci est qu'en ...

Après avoir posté une suggestion sur pgsql-docs , Marko Tiikkaja a expliqué (ce qui correspond à la réponse d'Erwin):

Les cas d'insertion-mise à jour et d'insertion-suppression ne fonctionnent pas car les mises à jour et les suppressions n'ont aucun moyen de voir les lignes insérées car leur instantané a été pris avant que l'insertion ne se produise. Il n'y a rien d'imprévisible dans ces deux cas.

Ainsi, la raison pour laquelle votre déclaration ne se met pas à jour peut être expliquée par le premier paragraphe ci-dessus (à propos des "instantanés"). Ce qui se passe lorsque vous avez modifié des CTE, c'est que tous et la requête principale sont exécutés et "voient" le même instantané des données (tables), comme c'était immédiatement avant l'exécution de l'instruction. Les CTE peuvent se transmettre des informations sur ce qu'ils ont inséré / mis à jour / supprimé entre eux et à la requête principale en utilisant la RETURNINGclause, mais ils ne peuvent pas voir directement les modifications dans les tableaux. Voyons donc ce qui se passe dans votre déclaration:

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

Nous avons 2 parties, le CTE ( newval):

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

et la requête principale:

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

Le flux d'exécution est quelque chose comme ceci:

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

Par conséquent, lorsque la requête principale rejoint le tbl(comme le montre l'instantané) avec la newvaltable, elle joint une table vide avec une table à 1 ligne. De toute évidence, il met à jour 0 lignes. Donc, l'instruction n'est jamais vraiment venue modifier la ligne nouvellement insérée et c'est ce que vous voyez.

Dans votre cas, la solution consiste à réécrire l'instruction pour insérer les valeurs correctes en premier lieu ou à utiliser 2 instructions. Un qui insère et un second à mettre à jour.


Il existe d'autres situations similaires, comme si l'instruction avait un INSERTpuis un DELETEsur les mêmes lignes. La suppression échouerait exactement pour les mêmes raisons.

Certains autres cas, avec update-update et update-delete et leur comportement sont expliqués dans un paragraphe suivant, dans la même page de documentation.

Essayer de mettre à jour la même ligne deux fois dans une seule instruction n'est pas pris en charge. Une seule des modifications a lieu, mais il n'est pas facile (et parfois impossible) de prédire de manière fiable laquelle. Cela s'applique également à la suppression d'une ligne déjà mise à jour dans la même instruction: seule la mise à jour est effectuée. Par conséquent, vous devez généralement éviter d'essayer de modifier une seule ligne deux fois dans une seule instruction. En particulier, évitez d'écrire des sous-instructions WITH qui pourraient affecter les mêmes lignes modifiées par l'instruction principale ou une sous-instruction frère. Les effets d'une telle déclaration ne seront pas prévisibles.

Et dans la réponse de Marko Tiikkaja:

Les cas de mise à jour-mise à jour et de suppression-mise à jour ne sont explicitement pas causés par les mêmes détails d'implémentation sous-jacents (comme les cas d'insertion-mise à jour et d'insertion-suppression).
Le cas de mise à jour-mise à jour ne fonctionne pas car il ressemble en interne au problème d'Halloween, et Postgres n'a aucun moyen de savoir quels tuples seraient corrects de mettre à jour deux fois et lesquels pourraient réintroduire le problème d'Halloween.

La raison est donc la même (comment les CTE de modification sont implémentés et comment chaque CTE voit le même instantané) mais les détails diffèrent dans ces 2 cas, car ils sont plus complexes et les résultats peuvent être imprévisibles dans le cas de mise à jour-mise à jour.

Dans l'insertion-mise à jour (selon votre cas) et une insertion-suppression similaire, les résultats sont prévisibles. Seule l'insertion se produit car la deuxième opération (mise à jour ou suppression) n'a aucun moyen de voir et d'affecter les lignes nouvellement insérées.


La solution suggérée est cependant la même pour tous les cas qui essaient de modifier les mêmes lignes plus d'une fois: ne le faites pas. Écrivez des instructions qui modifient chaque ligne une fois ou utilisez des instructions distinctes (2 ou plus).

ypercubeᵀᴹ
la source