web-dev-qa-db-fra.com

Postgres UPDATE ... LIMIT 1

J'ai une base de données Postgres qui contient des détails sur les clusters de serveurs, tels que l'état du serveur ("actif", "en veille", etc.). Les serveurs actifs à tout moment peuvent avoir besoin de basculer vers une veille, et je me fiche de savoir quelle veille est utilisée en particulier.

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

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?

91
vastlysuperiorman

Sans accès en écriture simultané

Matérialiser une sélection dans un [~ # ~] cte [~ # ~] (Common Table Expressions) et le rejoindre dans la clause FROM 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 s.server_ip;

J'avais à l'origine une sous-requête simple ici, mais cela peut contourner le LIMIT pour certains plans de requête comme Feike l'a souligné:

Le planificateur peut choisir de générer un plan qui exécute une boucle imbriquée sur la sous-requête LIMITing, provoquant plus UPDATEs que 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 du cas de test

La façon de résoudre ce qui précède était d'envelopper la sous-requête LIMIT dans son propre CTE, car le CTE est matérialisé, il ne renverra pas de résultats différents sur différentes itérations de la boucle imbriquée.

Ou utilisez un modeste corrélé sous-requête pour le cas simple avec LIMIT1. 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 niveau d'isolement par défaut READ COMMITTED pour tout cela. Des niveaux d'isolement plus stricts (REPEATABLE READ Et SERIALIZABLE) peuvent toujours entraîner des erreurs de sérialisation. Voir:

Sous charge d'écriture simultanée, ajoutez FOR UPDATE SKIP LOCKED Pour verrouiller la ligne afin d'éviter les conditions de concurrence. SKIP LOCKED A é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, donc cela ne convient pas pour un travail général, mais peut être utilisé pour éviter les conflits de verrouillage avec plusieurs consommateurs accédant à une table de type 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 et dé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 terminent pas la mise à jour (ROLLBACK ou autres raisons). Pour être sûr exécutez une vérification finale:

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

SELECT voit également les lignes verrouillées. Wile qui ne retourne pas 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 bouclez les deux étapes: (UPDATE jusqu'à ce que vous ne récupériez aucune ligne; SELECT ...) jusqu'à ce que vous obteniez true.

En relation:

Sans SKIP LOCKED Dans PostgreSQL 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 essayant 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 déroule normalement; d'autres dans la file d'attente continuent d'attendre.

Si le premier est validé, la condition WHERE est réévaluée et si ce n'est plus TRUE (status a changé), le CTE (quelque peu surprenant) ne renvoie aucune ligne. Rien ne se passe. C'est le comportement souhaité lorsque toutes les transactions veulent mettre à jour la ligne même.
Mais pas lorsque chaque transaction veut mettre à jour la ligne suivante. Et puisque nous voulons juste 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 ligne suivante n'est pas encore verrouillée sera mis à jour. Chaque transaction obtient une nouvelle ligne avec laquelle travailler. J'ai eu l'aide de Czech Postgres Wiki pour cette astuce.

id étant une colonne bigint unique (ou n'importe quel type avec un transtypage implicite comme int4 ou int2).

Si des verrous consultatifs sont utilisés simultanément pour plusieurs tables de votre base de données, supprimez toute ambiguïté avec pg_try_advisory_xact_lock(tableoid::int, id) - id étant un integer unique ici.
Puisque tableoid est une quantité bigint, elle peut théoriquement déborder integer. Si vous êtes assez paranoïaque, utilisez plutôt (tableoid::bigint % 2147483648)::int - laissant une "collision de hachage" théorique pour le véritable paranoïaque ...

De plus, Postgres est libre de tester les conditions de WHERE dans n'importe quel ordre. Il pourrait tester pg_try_advisory_xact_lock() et acquérir un verrou avantstatus = 'standby', Ce qui pourrait entraîner des verrous consultatifs supplémentaires sur les lignes non liées , où 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 éligibles sont verrouillées, vous pouvez imbriquer le (s) prédicat (s) dans un CTE comme ci-dessus ou une sous-requête avec le hack OFFSET 0 (Empêche l'inline) ) . Exemple:

Ou (moins cher pour les analyses séquentielles) imbriquez les conditions dans une instruction CASE comme:

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

Cependant l'astuce CASE empêcherait également Postgres d'utiliser un index sur status. Si un tel index est disponible, vous n'avez pas besoin d'imbrication supplémentaire pour commencer: seules les lignes éligibles 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 CASE est logiquement redondant, mais il sert le but discuté.

Si la commande fait partie d'une longue transaction, 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() et pg_advisory_unlock() . Le manuel:

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

En relation:

138
Erwin Brandstetter