J'ai une question connexe , mais c'est une autre partie de MON puzzle.
Je voudrais obtenir la VIEILLE VALEUR d'une colonne d'une ligne qui a été MISE À JOUR - SANS utiliser de déclencheurs (ni procédures stockées, ni aucune autre entité supplémentaire, non SQL/requête).
La requête que j'ai est la suivante:
UPDATE my_table
SET processing_by = our_id_info -- unique to this worker
WHERE trans_nbr IN (
SELECT trans_nbr
FROM my_table
GROUP BY trans_nbr
HAVING COUNT(trans_nbr) > 1
LIMIT our_limit_to_have_single_process_grab
)
RETURNING row_id;
Si je pouvais faire "FOR UPDATE ON my_table" à la fin de la sous-requête, ce serait devine (et corriger mon autre question/problème). Mais, cela ne fonctionnera pas: ne peut pas avoir ceci ET un "GROUP BY" (qui est nécessaire pour déterminer le COUNT de trans_nbr's). Ensuite, je pourrais simplement prendre ces trans_nbr et faire d'abord une requête pour obtenir les anciennes valeurs de processing_by (qui seront bientôt).
J'ai essayé de faire comme:
UPDATE my_table
SET processing_by = our_id_info -- unique to this worker
FROM my_table old_my_table
JOIN (
SELECT trans_nbr
FROM my_table
GROUP BY trans_nbr
HAVING COUNT(trans_nbr) > 1
LIMIT our_limit_to_have_single_process_grab
) sub_my_table
ON old_my_table.trans_nbr = sub_my_table.trans_nbr
WHERE my_table.trans_nbr = sub_my_table.trans_nbr
AND my_table.processing_by = old_my_table.processing_by
RETURNING my_table.row_id, my_table.processing_by, old_my_table.processing_by
Mais cela ne peut pas fonctionner; old_my_table
n'est pas visible en dehors de la jointure; la clause RETURNING
est aveugle.
J'ai depuis longtemps perdu le compte de toutes les tentatives que j'ai faites; Je fais des recherches sur cela depuis des heures.
Si je pouvais simplement trouver un moyen à l'épreuve des balles de verrouiller les lignes de ma sous-requête - et UNIQUEMENT ces lignes, et QUAND la sous-requête se produit - tous les problèmes de concurrence que j'essaie d'éviter disparaîtraient ...
MISE À JOUR: [WIPES Egg OFF FACE] D'accord, j'ai donc eu une faute de frappe dans le code non générique ci-dessus que j'ai écrit "ne fonctionne pas "; il le fait ... grâce à Erwin Brandstetter , ci-dessous, qui a déclaré que ce serait le cas, je l'ai refait (après une nuit de sommeil, les yeux rafraîchis et une banane pour bfast). Puisqu'il a fallu moi si long/difficile pour trouver ce genre de solution, peut-être que ma gêne en vaut la peine? Au moins c'est sur SO pour la postérité maintenant ...:>
Ce que j'ai maintenant (qui fonctionne) est comme ceci:
UPDATE my_table
SET processing_by = our_id_info -- unique to this worker
FROM my_table AS old_my_table
WHERE trans_nbr IN (
SELECT trans_nbr
FROM my_table
GROUP BY trans_nbr
HAVING COUNT(*) > 1
LIMIT our_limit_to_have_single_process_grab
)
AND my_table.row_id = old_my_table.row_id
RETURNING my_table.row_id, my_table.processing_by, old_my_table.processing_by AS old_processing_by
Le COUNT (*) est par une suggestion de Flimzy dans un commentaire sur mon autre (lié ci-dessus) question. (J'étais plus précis que nécessaire. [Dans ce cas.])
Veuillez consulter mon autre question pour implémenter correctement la concurrence et même une version non bloquante; CETTE requête montre simplement comment obtenir les anciennes et les nouvelles valeurs d'une mise à jour, ignorer les bits de concurrence incorrects/incorrects.
La clause facultative
RETURNING
permet àUPDATE
de calculer et de renvoyer des valeurs en fonction de chaque ligne réellement mise à jour. Toute expression utilisant les colonnes de la table et/ou les colonnes d'autres tables mentionnées dansFROM
, peut être calculée. Les nouvelles valeurs (post-mise à jour) des colonnes du tableau sont utilisées . La syntaxe de la listeRETURNING
est identique à celle de la liste de sortie deSELECT
.
Je souligne. Il n'y a aucun moyen d'accéder à l'ancienne ligne dans une clause RETURNING
. Vous pouvez le faire dans un déclencheur ou avec un SELECT
distinct avant le UPDATE
, encapsulé dans une transaction comme @Flimzy et @wildplasser ont commenté, ou enveloppé dans un CTE comme l'a signalé @MattDiPasquale.
Cependant, ce que vous essayez d'atteindre fonctionne parfaitement si vous joignez une autre instance de la table dans la clause FROM
:
UPDATE tbl x
SET tbl_id = 23
, name = 'New Guy'
FROM tbl y -- using the FROM clause
WHERE x.tbl_id = y.tbl_id -- must be UNIQUE NOT NULL
AND x.tbl_id = 3
RETURNING y.tbl_id AS old_id, y.name AS old_name
, x.tbl_id , x.name;
Retour:
old_id | old_name | tbl_id | name
--------+----------+--------+---------
3 | Old Guy | 23 | New Guy
J'ai testé cela avec les versions PostgreSQL de 8.4 à 9.6.
C'est différent pour INSERT
:
Il existe plusieurs façons d'éviter les conditions de concurrence avec des opérations d'écriture simultanées. La méthode simple, lente et sûre (mais coûteuse) consiste à exécuter la transaction avec le niveau d'isolation SERIALIZABLE
.
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE ..;
COMMIT;
Mais c'est probablement exagéré. Et vous devrez être prêt à répéter l'opération si vous obtenez un échec de sérialisation.
Un verrou explicite sur la ligne d'une à mettre à jour est plus simple et plus rapide (et tout aussi fiable avec une charge d'écriture simultanée):
UPDATE tbl x
SET tbl_id = 24
, name = 'New Gal'
FROM (SELECT tbl_id, name FROM tbl WHERE tbl_id = 4 FOR UPDATE) y
WHERE x.tbl_id = y.tbl_id
RETURNING y.tbl_id AS old_id, y.name AS old_name, x.tbl_id, x.name;
Plus d'explications, d'exemples et de liens sous cette question connexe:
Vous pouvez utiliser une sous-requête SELECT
.
Exemple: pdate email d'un utilisateur RETURNING
l'ancienne valeur.
RETURNING
Sous-requête
UPDATE users SET email = '[email protected]' WHERE id = 1
RETURNING (SELECT email FROM users WHERE id = 1);
PostgreSQL WITH Query (Expressions de table communes)
WITH u AS (
SELECT email FROM users WHERE id = 1
)
UPDATE users SET email = '[email protected]' WHERE id = 1
RETURNING (SELECT email FROM u);
Cela a fonctionné plusieurs fois sur ma base de données locale sans échec, mais je ne sais pas si le SELECT
dans WITH
est garanti de s'exécuter de manière cohérente avant le UPDATE
puisque "le sous -les déclarations dans WITH sont exécutées simultanément entre elles et avec la requête principale. "
La variante CTE comme proposée par @MattDiPasquale devrait également fonctionner.
Avec les moyens confortables d'un CTE, je serais plus explicite, cependant:
WITH sel AS (
SELECT tbl_id, name FROM tbl WHERE tbl_id = 3 -- assuming unique tbl_id
)
, upd AS (
UPDATE tbl SET name = 'New Guy' WHERE tbl_id = 3
RETURNING tbl_id, name
)
SELECT s.tbl_id AS old_id, s.name As old_name
, u.tbl_id, u.name
FROM sel s, upd u;
Sans test, je prétends que cela fonctionne: SELECT
et UPDATE
voir le même instantané de la base de données. Le SELECT
est destiné à renvoyer les anciennes valeurs (même si vous placez le CTE après le CTE avec le UPDATE
), tandis que le UPDATE
renvoie les nouvelles valeurs par définition. Voilá.
Mais ce sera plus lent que ma première réponse.
face à ce dilemme, j'ai ajouté des colonnes indésirables à la table, puis je copie les anciennes valeurs dans les colonnes indésirables (que je renvoie ensuite) lorsque je mets à jour l'enregistrement. cela gonfle un peu la table mais évite le besoin de jointures.