Sans accès en écriture simultané
Matérialiser une sélection dans un CTE et la rejoindre dans la FROM
clause 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 LIMIT
plans 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 LIMITing
sous - requête, ce qui entraîne 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 d'un cas de test
Pour résoudre ce problème, la solution consistait à envelopper la LIMIT
sous - 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 READ
et SERIALIZABLE
) peuvent toujours entraîner des erreurs de sérialisation. Voir:
Sous charge d'écriture simultanée, ajoutez cette FOR UPDATE SKIP LOCKED
option pour verrouiller la ligne afin d'éviter les situations 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, 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 ( ROLLBACK
ou 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'
);
SELECT
voit é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 UPDATE
passez 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 LOCKED
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 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 WHERE
condition est réévaluée et si ce n'est TRUE
plus le cas ( status
a 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 bigint
colonne unique (ou n'importe quel type avec une distribution implicite telle que int4
ou 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)
- id
soyez un unique integer
ici.
Puisqu'il tableoid
s'agit d'une bigint
quantité, il peut théoriquement déborder integer
. Si vous êtes assez paranoïaque, utilisez (tableoid::bigint % 2147483648)::int
plutôt - laissant une "collision de hachage" théorique pour les véritablement paranoïaques ...
En outre, Postgres est libre de tester les WHERE
conditions 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 0
hack (empêche l'inlining) . Exemple:
Ou (moins cher pour les analyses séquentielles) imbriquer les conditions dans une CASE
déclaration telle que:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Cependant, l’ CASE
astuce 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 CASE
est 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: