Sin acceso de escritura concurrente
Materialice una selección en un CTE y únase a ella en la FROM
cláusula del 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;
Originalmente tenía una subconsulta simple aquí, pero eso puede eludir LIMIT
ciertos planes de consulta como señaló Feike :
El planificador puede elegir generar un plan que ejecute un bucle anidado sobre la LIMITing
subconsulta, causando más UPDATEs
de LIMIT
, por ejemplo:
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
Reproducir caso de prueba
La forma de arreglar lo anterior fue envolver la LIMIT
subconsulta en su propio CTE, ya que el CTE se materializa no devolverá resultados diferentes en diferentes iteraciones del bucle anidado.
O utilice una subconsulta poco correlacionada para el caso simple conLIMIT
1
. Más simple, más rápido:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
Con acceso de escritura concurrente
Asumiendo el nivel de aislamiento predeterminadoREAD COMMITTED
para todo esto. Los niveles de aislamiento más estrictos ( REPEATABLE READ
y SERIALIZABLE
) aún pueden provocar errores de serialización. Ver:
Bajo carga de escritura concurrente, agregue FOR UPDATE SKIP LOCKED
para bloquear la fila para evitar condiciones de carrera. SKIP LOCKED
fue agregado en Postgres 9.5 , para versiones anteriores ver abajo. El manual:
Con SKIP LOCKED
, se omiten las filas seleccionadas que no se pueden bloquear inmediatamente. Omitir filas bloqueadas proporciona una vista inconsistente de los datos, por lo que esto no es adecuado para el trabajo de propósito general, pero puede usarse para evitar la contención de bloqueos con múltiples consumidores que acceden a una tabla tipo cola.
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;
Si no queda una fila desbloqueada que califique, no sucede nada en esta consulta (no se actualiza ninguna fila) y obtiene un resultado vacío. Para operaciones no críticas, eso significa que has terminado
Sin embargo, las transacciones concurrentes pueden tener filas bloqueadas, pero luego no terminan la actualización ( ROLLBACK
u otras razones). Para estar seguro, ejecute una verificación final:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
También ve filas bloqueadas. Si bien eso no regresa true
, una o más filas aún se están procesando y las transacciones aún podrían revertirse. (O mientras tanto, se han agregado nuevas filas). Espere un poco, luego repita los dos pasos: ( UPDATE
hasta que no recupere la fila; SELECT
...) hasta que llegue true
.
Relacionado:
Sin SKIP LOCKED
en PostgreSQL 9.4 o anterior
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;
Las transacciones concurrentes que intentan bloquear la misma fila se bloquean hasta que la primera libera su bloqueo.
Si se revierte la primera, la siguiente transacción toma el bloqueo y continúa normalmente; otros en la cola siguen esperando.
Si se confirma por primera vez, la WHERE
condición se vuelve a evaluar y si ya no TRUE
existe ( status
ha cambiado) el CTE (algo sorprendente) no devuelve ninguna fila. No pasa nada. Ese es el comportamiento deseado cuando todas las transacciones desean actualizar la misma fila .
Pero no cuando cada transacción quiere actualizar la siguiente fila . Y dado que solo queremos actualizar una fila arbitraria (o aleatoria ) , no tiene sentido esperar en absoluto.
Podemos desbloquear la situación con la ayuda de bloqueos de asesoramiento :
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 esta manera, la siguiente fila no bloqueada aún se actualizará. Cada transacción obtiene una nueva fila para trabajar. Recibí ayuda de la República Checa Postgres Wiki para este truco.
id
siendo cualquier bigint
columna única (o cualquier tipo con una conversión implícita como int4
o int2
).
Si se usan bloqueos de aviso para varias tablas en su base de datos al mismo tiempo, desambigüe con pg_try_advisory_xact_lock(tableoid::int, id)
- id
siendo único integer
aquí.
Como tableoid
es una bigint
cantidad, teóricamente puede desbordarse integer
. Si eres lo suficientemente paranoico, úsalo (tableoid::bigint % 2147483648)::int
en su lugar, dejando una "colisión hash" teórica para el verdadero paranoico ...
Además, Postgres es libre de probar las WHERE
condiciones en cualquier orden. Se podría probar pg_try_advisory_xact_lock()
y adquirir un bloqueo antes status = 'standby'
, lo que podría dar lugar a bloques de consulta adicionales en filas no relacionadas, en donde status = 'standby'
no es cierto. Pregunta relacionada sobre SO:
Por lo general, puedes ignorar esto. Para garantizar que solo las filas calificadas estén bloqueadas, puede anidar el predicado (s) en un CTE como el anterior o una subconsulta con el OFFSET 0
pirateo (evita la inserción) . Ejemplo:
O (más barato para escaneos secuenciales) anida las condiciones en una CASE
declaración como:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Sin embargo, el CASE
truco también evitaría que Postgres use un índice status
. Si dicho índice está disponible, no necesita anidamiento adicional para comenzar: solo las filas calificadas se bloquearán en un escaneo de índice.
Como no puede estar seguro de que se use un índice en cada llamada, puede simplemente:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
El CASE
es lógicamente redundante, pero sirve al propósito discutido.
Si el comando es parte de una transacción larga, considere los bloqueos a nivel de sesión que pueden (y deben) liberarse manualmente. Por lo tanto, puede desbloquear tan pronto como haya terminado con la fila bloqueada: pg_try_advisory_lock()
ypg_advisory_unlock()
. El manual:
Una vez adquirido a nivel de sesión, se mantiene un bloqueo de aviso hasta que se libere explícitamente o la sesión finalice.
Relacionado: