2017-06-20 10 views
1

Je dois faire une mise à jour Postgres sur une collection d'enregistrements & J'essaie d'éviter un blocage qui est apparu dans les tests de résistance.Postgres UPDATE avec ORDER BY, comment le faire?

La résolution typique à ceci est de mettre à jour des 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 dois faire une mise à jour, par exemple:

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

résultats à lorsque vous exécutez les interblocages 200 requêtes en même temps. Que faire?

Je suis à la recherche d'une solution générale, des solutions de contournement non spécifiques au cas comme dans UPDATE with ORDER BY

Il sent qu'il doit y avoir une meilleure solution que l'écriture d'une fonction de curseur. De plus, s'il n'y a pas de meilleur moyen, comment le curseur fonctionnerait-il de manière optimale? Mise à jour enregistrement par enregistrement

+0

Avez-vous essayé la syntaxe UPDATE ... FROM ...? –

+0

Oui, j'ai essayé UPDATE .... FROM .... SELECT ... POUR LA MISE À JOUR mais pas de changement dans la trace de la pile. Cela rend le problème plus commun. – bbozo

Répondre

2

Pour autant que je sache, il n'y a aucun moyen d'y arriver directement par la déclaration UPDATE; la seule façon de garantir l'ordre de verrouillage est d'acquérir explicitement les serrures 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 a 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 allant chercher l'adresse de ligne physique (représentée par la ctid system column) lors de la requête de verrouillage, et à l'aide que pour conduire 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 ctid s, car les valeurs sont transitoires, car les verrous bloqueront tous les changements.)

Malheureusement, le planificateur utilisera seulement le ctid dans un ensemble étroit de cas (vous pouvez dire si cela fonctionne en cherchant un nœud "Tid Scan" dans la sortie EXPLAIN). Pour gérer des requêtes plus complexes dans une seule instruction UPDATE, par ex. si votre nouveau solde était retourné par some_function() à côté de l'ID, vous devrez 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 les frais généraux de performance est un problème, vous auriez besoin de recourir à l'utilisation 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 
$$ 
2

En général, la concurrence est difficile. Surtout avec 200 déclarations (je présume que vous ne faites pas que query = SELECT) ou même des transactions (en fait, chaque déclaration unique est enveloppée dans une transaction si elle n'est pas déjà dans une transaction).

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

  1. être conscients que les blocages peuvent se produire, les attraper dans l'application, vérifiez la Error Codes pour class 40 ou 40P01 et essayer de nouveau la transaction.

  2. Verrous de réserve. Utilisez SELECT ... FOR UPDATE. Eviter les verrous explicites aussi longtemps que possible. Les verrous forceront les autres transactions à attendre la libération du verrou, ce qui nuit à la simultanéité, mais peut empêcher les transactions de se bloquer. Consultez l'exemple des blocages dans le chapitre 13. Surtout celui dans lequel la transaction A attend B et B attend A (le compte bancaire).

  3. Choisissez un Isolation Level différent, par exemple un plus faible comme READ COMMITED, si possible. Soyez conscient de LOST UPDATE en mode READ COMMITED. Empêchez-les avec REPEATABLE READ.

Ecrivez vos instructions avec des verrous dans le même ordre dans CHAQUE transaction, par exemple par nom de table par ordre alphabétique.

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'intercaler dans n'importe quel ordre relatif et avoir encore une bonne chance de ne pas se bloquer (en fonction de vos instructions, vous pouvez avoir d'autres problèmes de sérialisation). Les déclarations des transactions seront exécutées dans l'ordre spécifié par elles, mais il se peut que la transaction 1 exécute leur premier 2, puis xact 2 exécute le premier, puis 1 finit et enfin xact 2 finit. En outre, vous devez réaliser qu'une instruction impliquant plusieurs lignes n'est pas exécutée de manière atomique dans une situation simultanée.En d'autres termes, si vous avez deux déclarations A et B comportant plusieurs lignes, alors ils peuvent être exécutés dans cet ordre:

a1 b1 a2 a3 a4 b2 b3  

mais pas comme un bloc d'un est suivi par des années b. La même chose s'applique à une instruction avec une sous-requête. Avez-vous regardé les plans de requête en utilisant 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 par ce que vous voulez faire, vous pouvez également utiliser SELECT ... FOR UPDATE SKIP LOCK, qui sautera données déjà verrouillé pour récupérer la concurrence, ce qui est perdu en attendant une autre transaction pour libérer un verrou (FOR UPDATE). Mais cela ne va pas appliquer une mise à jour aux lignes verrouillées, ce qui pourrait nécessiter votre logique d'application. Alors lancez-le plus tard (voir point 1).

lire également LOST UPDATE sur le LOST UPDATE et SKIP LOCKED à propos 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 censés être des files d'attente.

HTH

+0

Merci, je vais jouer avec ça. Y a-t-il un verrouillage sous le capot pour les sélections? Je suppose que non, mais je deviens fou – bbozo

+1

Je ne pense pas que votre 'FOR UPDATE' fasse quoi que ce soit, puisque le' SELECT' ne référence aucune table ... –