web-dev-qa-db-fra.com

Renvoyer les valeurs de colonne pré-UPDATE en utilisant SQL uniquement - Version PostgreSQL

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.

43
pythonlarry

Problème

Le manuel explique :

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 dans FROM, peut être calculée. Les nouvelles valeurs (post-mise à jour) des colonnes du tableau sont utilisées . La syntaxe de la liste RETURNING est identique à celle de la liste de sortie de SELECT.

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.

Solution

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

SQL Fiddle.

J'ai testé cela avec les versions PostgreSQL de 8.4 à 9.6.

C'est différent pour INSERT:

Gérer la charge d'écriture simultanée

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:

67
Erwin Brandstetter

Vous pouvez utiliser une sous-requête SELECT.

Exemple: pdate email d'un utilisateur RETURNING l'ancienne valeur.

  1. RETURNING Sous-requête

    UPDATE users SET email = '[email protected]' WHERE id = 1
    RETURNING (SELECT email FROM users WHERE id = 1);
    
  2. 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. "

10
ma11hew28

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.

7
Erwin Brandstetter

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.

1
Jasen