web-dev-qa-db-fra.com

Postgres UPDATE avec ORDER BY, comment le faire?

Je dois faire une mise à jour Postgres sur une collection d'enregistrements et j'essaie d'éviter une impasse apparue dans les tests de résistance. 

La solution typique consiste à mettre à jour les enregistrements dans un certain ordre, par ID par exemple - mais il semble que Postgres n'autorise pas ORDER BY pour UPDATE.

En supposant que je doive faire une mise à jour, par exemple:

UPDATE BALANCES WHERE ID IN (SELECT ID FROM some_function() ORDER BY ID);

entraîne des blocages lorsque vous exécutez 200 requêtes simultanément. Que faire?

Je recherche une solution générale, pas des solutions spécifiques à un cas comme dans UPDATE avec ORDER BY

Il {sent} _ qu'il doit exister une meilleure solution que l'écriture d'une fonction de curseur. En outre, s’il n’ya pas de meilleure solution, à quoi ressemblerait ce curseur de manière optimale? Mettre à jour l'enregistrement par enregistrement

9
bbozo

Autant que je sache, il n’ya aucun moyen d’accomplir cela directement par la déclaration UPDATE; le seul moyen de garantir l'ordre de verrouillage est d'acquérir explicitement des verrous avec un SELECT ... ORDER BY ID FOR UPDATE, par exemple:

UPDATE Balances
SET Balance = 0
WHERE ID IN (
  SELECT ID FROM Balances
  WHERE ID IN (SELECT ID FROM some_function())
  ORDER BY ID
  FOR UPDATE
)

Cela présente l'inconvénient de répéter la recherche d'index ID sur la table Balances. Dans votre exemple simple, vous pouvez éviter cette surcharge en récupérant l'adresse de ligne physique (représentée par la colonne système ctid ) pendant la requête de verrouillage et en l'utilisant pour piloter la UPDATE:

UPDATE Balances
SET Balance = 0
WHERE ctid = ANY(ARRAY(
  SELECT ctid FROM Balances
  WHERE ID IN (SELECT ID FROM some_function())
  ORDER BY ID
  FOR UPDATE
))

(Soyez prudent lorsque vous utilisez ctids, car les valeurs sont transitoires. Nous sommes en sécurité ici, car les verrous bloquent toute modification.)

Malheureusement, le planificateur n'utilisera la variable ctid que dans un nombre restreint de cas (vous pouvez savoir s'il fonctionne en recherchant un nœud "Analyse Tid" dans la sortie EXPLAIN). Pour gérer des requêtes plus complexes dans une seule instruction UPDATE, par ex. Si votre nouveau solde a été retourné par some_function() à côté de l'ID, vous devez revenir à la recherche basée sur l'ID:

UPDATE Balances
SET Balance = Locks.NewBalance
FROM (
  SELECT Balances.ID, some_function.NewBalance
  FROM Balances
  JOIN some_function() ON some_function.ID = Balances.ID
  ORDER BY Balances.ID
  FOR UPDATE
) Locks
WHERE Balances.ID = Locks.ID

Si le surcoût lié aux performances pose problème, vous devrez recourir à un curseur, qui ressemblerait à ceci:

DO $$
DECLARE
  c CURSOR FOR
    SELECT Balances.ID, some_function.NewBalance
    FROM Balances
    JOIN some_function() ON some_function.ID = Balances.ID
    ORDER BY Balances.ID
    FOR UPDATE;
BEGIN
  FOR row IN c LOOP
    UPDATE Balances
    SET Balance = row.NewBalance
    WHERE CURRENT OF c;
  END LOOP;
END
$$
9
Nick Barnes

En général, la concurrence est difficile. Surtout avec 200 instructions (en supposant que vous ne faites pas seulement une requête = SELECT) ou même des transactions (en réalité, chaque instruction émise est encapsulée dans une transaction si elle ne l’est pas déjà). 

Les concepts généraux de solution sont (une combinaison de) ceux-ci: 

  1. Pour être conscient que des blocages peuvent se produire, attrapez-les dans l'application, vérifiez les Codes d'erreur pour class 40 ou 40P01 et réessayez la transaction.

  2. Réserver des serrures. Utilisez SELECT ... FOR UPDATE. Évitez les verrous explicites aussi longtemps que possible. Les verrous forceront les autres transactions à attendre leur libération, ce qui nuira à la concurrence, mais empêchera les transactions de se bloquer. Consultez l'exemple des blocages au chapitre 13. En particulier celui dans lequel la transaction A attend B et B attend A (le compte bancaire).

  3. Choisissez un autre Niveau d'isolation , par exemple un plus faible comme READ COMMITED, si possible. Soyez conscient de LOST UPDATEs en mode READ COMMITED. Prévenez-les avec REPEATABLE READ.

Rédigez vos déclarations avec des verrous dans le même ordre dans CHAQUE transaction, par exemple, par ordre alphabétique du nom de la table. 

LOCK / USE A  -- Transaction 1 
LOCK / USE B  -- Transaction 1
LOCK / USE C  -- Transaction 1
-- D not used -- Transaction 1

-- A not used -- Transaction 2
LOCK / USE B  -- Transaction 2
-- C not used -- Transaction 2
LOCK / USE D  -- Transaction 2

avec l'ordre de verrouillage général A B C D. De cette façon, les transactions peuvent s'entrelacer dans n'importe quel ordre relatif tout en ayant une bonne chance de ne pas vous bloquer (vous pouvez toutefois rencontrer d'autres problèmes de sérialisation en fonction de vos déclarations). Les déclarations des transactions seront exécutées dans l'ordre spécifié par elles, mais il se peut que la transaction 1 en exécute les 2 premières, puis que xact 2 exécute la première, puis que 1 se termine et enfin, xact 2 se termine.

En outre, vous devez comprendre qu'une instruction impliquant plusieurs lignes n'est pas exécutée de manière atomique dans une situation concurrente. En d'autres termes, si vous avez deux instructions A et B impliquant plusieurs lignes, vous pouvez les exécuter dans cet ordre: 

a1 b1 a2 a3 a4 b2 b3     

mais PAS comme un bloc de a suivi de b. Il en va de même pour une instruction avec une sous-requête ..__ Avez-vous examiné les plans de requête à l'aide de EXPLAIN?

Dans votre cas, vous pouvez essayer 

UPDATE BALANCES WHERE ID IN (
 SELECT ID FROM some_function() FOR UPDATE  -- LOCK using FOR UPDATE 
 -- other transactions will WAIT / BLOCK temporarily on conc. write access
);

Si possible en fonction de ce que vous souhaitez faire, vous pouvez également utiliser SELECT ... FOR UPDATE SKIP LOCK , qui ignorera les données déjà verrouillées pour récupérer la simultanéité, perdue en WAITant pour qu'une autre transaction libère un verrou ( POUR MISE À JOUR). Mais cela n’appliquera pas UPDATE aux lignes verrouillées, ce que votre logique d’application pourrait nécessiter. Alors lancez ça plus tard (voir point 1).

Lisez également LOST UPDATE à propos du LOST UPDATE et SKIP LOCKED à propos de SKIP LOCKED. Une file d’attente peut être une idée dans votre cas, ce qui est parfaitement expliqué dans la référence SKIP LOCKED, bien que les SGBD relationnels ne soient pas destinés à être des files d’attente.

HTH

2
flutter