Postgres UPDATE… LIMIT 1


78

J'ai une base de données Postgres qui contient des détails sur les clusters de serveurs, tels que le statut du serveur ('actif', 'en veille', etc.). Les serveurs actifs à tout moment peuvent avoir besoin de basculer en mode veille, et peu importe le type de veille utilisé.

Je souhaite qu'une requête de base de données modifie le statut d'un serveur en veille - JUST ONE - et renvoie l'adresse IP du serveur à utiliser. Le choix peut être arbitraire: puisque l'état du serveur change avec la requête, le mode de veille sélectionné n'a pas d'importance.

Est-il possible de limiter ma requête à une seule mise à jour?

Voici ce que j'ai jusqu'à présent:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

Postgres n'aime pas ça. Que pourrais-je faire différemment?


Il suffit de choisir le serveur dans le code et de l’ajouter comme contrainte contrainte. Cela vous permet également de faire vérifier en premier les conditions supplémentaires (les plus anciennes, les plus récentes, les plus récentes en vie, les moins chargées, le même courant continu, un rack différent, les moins d'erreurs). La plupart des protocoles de basculement nécessitent de toute façon une forme de déterminisme.
eckes

@ cheques C'est une idée intéressante. Dans mon cas, "choisir le serveur dans le code" aurait signifié d' abord lire une liste de serveurs disponibles sur la base de données, puis mettre à jour un enregistrement. Étant donné que de nombreuses instances de l'application pourraient effectuer cette action, il existe une situation de concurrence critique et une opération atomique est nécessaire (ou l'était il y a 5 ans). Le choix n'avait pas besoin d'être déterministe.
vastlysuperiorman

Réponses:


126

Sans accès en écriture simultané

Matérialiser une sélection dans un CTE et la rejoindre dans la FROMclause de la UPDATE.

WITH cte AS (
   SELECT server_ip          -- pk column or any (set of) unique column(s)
   FROM   server_info
   WHERE  status = 'standby'
   LIMIT  1                  -- arbitrary pick (cheapest)
   )
UPDATE server_info s
SET    status = 'active' 
FROM   cte
WHERE  s.server_ip = cte.server_ip
RETURNING server_ip;

A l'origine, j'avais une sous-requête simple, mais cela peut éviter les LIMITplans de requête, comme l' a souligné Feike :

Le planificateur peut choisir de générer un plan qui exécute une boucle imbriquée sur la LIMITingsous - requête, ce qui entraîne plus UPDATEsque LIMIT, par exemple:

 Update on buganalysis [...] rows=5
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis

Reproduction d'un cas de test

Pour résoudre ce problème, la solution consistait à envelopper la LIMITsous - requête dans son propre CTE. Lorsque le CTE est matérialisé, il ne retournera pas des résultats différents sur différentes itérations de la boucle imbriquée.

Ou utilisez une sous-requête faiblement corrélée pour le cas simple avecLIMIT 1. Plus simple, plus rapide:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         )
RETURNING server_ip;

Avec accès en écriture simultané

En supposant un niveau d'isolation par défautREAD COMMITTED pour tout cela. Des niveaux d'isolation plus stricts ( REPEATABLE READet SERIALIZABLE) peuvent toujours entraîner des erreurs de sérialisation. Voir:

Sous charge d'écriture simultanée, ajoutez cette FOR UPDATE SKIP LOCKEDoption pour verrouiller la ligne afin d'éviter les situations de concurrence . SKIP LOCKEDa été ajouté dans Postgres 9.5 , pour les anciennes versions, voir ci-dessous. Le manuel:

Avec SKIP LOCKED, toutes les lignes sélectionnées qui ne peuvent pas être immédiatement verrouillées sont ignorées. Ignorer les lignes verrouillées fournit une vue incohérente des données, ce qui ne convient donc pas pour un travail général, mais peut être utilisé pour éviter les conflits de verrous avec plusieurs consommateurs accédant à une table semblable à une file d'attente.

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE SKIP LOCKED
         )
RETURNING server_ip;

S'il ne reste aucune ligne qualifiée, non verrouillée, rien ne se passe dans cette requête (aucune ligne n'est mise à jour) et vous obtenez un résultat vide. Pour les opérations non critiques, cela signifie que vous avez terminé.

Cependant, les transactions simultanées peuvent avoir des lignes verrouillées, mais ne terminez pas la mise à jour ( ROLLBACKou d'autres raisons). Pour être sûr de faire une dernière vérification:

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

SELECTvoit également les lignes verrouillées. Si ce n'est pas le cas true, une ou plusieurs lignes sont toujours en cours de traitement et les transactions peuvent toujours être annulées. (Ou de nouvelles lignes ont été ajoutées entre-temps.) Attendez un peu, puis UPDATEpassez en boucle les deux étapes: ( jusqu'à ce que vous n'ayez plus de ligne; SELECT...) jusqu'à ce que vous obteniez true.

Apparenté, relié, connexe:

Sans SKIP LOCKEDPostgreSQL 9.4 ou plus ancien

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Les transactions simultanées qui tentent de verrouiller la même ligne sont bloquées jusqu'à ce que la première libère son verrou.

Si la première a été annulée, la transaction suivante prend le verrou et se poursuit normalement; d'autres dans la file d'attente continuent d'attendre.

Si le premier est validé, la WHEREcondition est réévaluée et si ce n'est TRUEplus le cas ( statusa changé), le CTE ne retourne pas (de manière assez surprenante) aucune ligne. Rien ne se passe. C'est le comportement souhaité lorsque toutes les transactions souhaitent mettre à jour la même ligne .
Mais pas lorsque chaque transaction veut mettre à jour la ligne suivante . Et comme nous souhaitons simplement mettre à jour une ligne arbitraire (ou aléatoire ) , il est inutile d'attendre du tout.

Nous pouvons débloquer la situation à l'aide de verrous consultatifs :

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         AND    pg_try_advisory_xact_lock(id)
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

De cette façon, la prochaine ligne non encore verrouillée sera mise à jour. Chaque transaction reçoit une nouvelle ligne avec laquelle travailler. J'ai eu l'aide de Czech Postgres Wiki pour cette astuce.

idêtre n'importe quelle bigintcolonne unique (ou n'importe quel type avec une distribution implicite telle que int4ou int2).

Si des verrous de mise en garde sont utilisés simultanément pour plusieurs tables de votre base de données, désambiguïsez-vous pg_try_advisory_xact_lock(tableoid::int, id)- idsoyez un unique integerici.
Puisqu'il tableoids'agit d'une bigintquantité, il peut théoriquement déborder integer. Si vous êtes assez paranoïaque, utilisez (tableoid::bigint % 2147483648)::intplutôt - laissant une "collision de hachage" théorique pour les véritablement paranoïaques ...

En outre, Postgres est libre de tester les WHEREconditions dans n'importe quel ordre. Cela pourrait permettre de tester pg_try_advisory_xact_lock()et d'acquérir un verrou avant status = 'standby' , ce qui pourrait entraîner des verrous supplémentaires sur les lignes non liées, ce qui status = 'standby'n'est pas vrai. Question connexe sur SO:

En règle générale, vous pouvez simplement ignorer cela. Pour garantir que seules les lignes qualifiantes sont verrouillées, vous pouvez imbriquer le ou les prédicats dans un CTE comme ci-dessus ou dans une sous-requête avec le OFFSET 0hack (empêche l'inlining) . Exemple:

Ou (moins cher pour les analyses séquentielles) imbriquer les conditions dans une CASEdéclaration telle que:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Cependant, l’ CASEastuce empêcherait également Postgres d’utiliser un index status. Si un tel index est disponible, vous n'avez pas besoin d'une imbrication supplémentaire pour commencer: seules les lignes qualifiantes seront verrouillées dans une analyse d'index.

Comme vous ne pouvez pas être sûr qu'un index est utilisé dans chaque appel, vous pouvez simplement:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Le CASEest logiquement redondant, mais il sert le but discuté.

Si la commande fait partie d'une transaction longue, envisagez des verrous au niveau de la session qui peuvent être (et doivent être) libérés manuellement. Vous pouvez donc déverrouiller dès que vous avez terminé avec la ligne verrouillée: pg_try_advisory_lock()etpg_advisory_unlock() . Le manuel:

Une fois acquis au niveau de la session, un verrou consultatif est maintenu jusqu'à ce qu'il soit explicitement publié ou jusqu'à la fin de la session.

Apparenté, relié, connexe:

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.