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 WITH queries indique que plusieurs mises à jour ne sont pas prises en charge, mais ne mentionne pas les INSERT et les mises à jour.
Toutes les déclarations dans un CTE se produisent pratiquement en même temps. C'est-à-dire qu'ils sont basés sur le même instantané de la base de données.
UPDATE
voit le même état de la table sous-jacente que INSERT
, ce qui signifie que la ligne avec val = 1
n'est pas encore là. Le manuel précise ici:
Toutes les instructions sont exécutées avec le même instantané (voir Chapitre 1 ), 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 clause RETURNING
. Mais les tables sous-jacentes leur ressemblent tout de même.
Vous auriez besoin de deux instructions (en une seule transaction) pour ce que vous essayez de faire. L'exemple donné ne devrait être qu'un simple INSERT
pour commencer, mais cela peut être dû à l'exemple simplifié.
Il s'agit d'une décision de mise en œuvre. Il est décrit dans la documentation Postgres, WITH
Queries (Common Table Expressions) . Il y a deux paragraphes liés à la question.
Tout d'abord, la raison du comportement observé:
Les sous-instructions de
WITH
sont exécutées simultanément entre elles et avec la requête principale . Par conséquent, lors de l'utilisation d'instructions de modification de données dansWITH
, 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 Chapitre 13), de sorte qu'elles ne peuvent pas "voir" les effets les unes des autres sur les tables cibles. Cela allège la effets de l'imprévisibilité de l'ordre réel des mises à jour des lignes, et signifie que les donnéesRETURNING
sont le seul moyen de communiquer les changements entre les différentes sous-instructionsWITH
et la requête principale. Un exemple de cela est que dans ...
Après avoir posté une suggestion à pgsql-docs , Marko Tiikkaja a expliqué (qui est d'accord avec 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), tel qu'il é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 clause RETURNING
mais ils ne peuvent pas voir directement les modifications dans les tables. 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 illustré dans l'instantané) avec la table newval
, 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.
La solution dans votre cas est de réécrire l'instruction pour insérer les valeurs correctes en premier lieu ou d'utiliser 2 instructions. Un qui insère et un second à mettre à jour.
Il existe d'autres situations similaires, comme si l'instruction avait un INSERT
puis un DELETE
sur 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 deux fois la même ligne dans une seule instruction n'est pas pris en charge. Une seule des modifications a lieu, mais ce n'est pas facile (et parfois pas possible) pour 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 update-update et update-delete sont explicitement pas causés par les mêmes détails d'implémentation sous-jacents (que 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 pourraient être mis à 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).