Sem acesso de gravação simultâneo
Materialize uma seleção em uma CTE e junte-se a ela na FROM
cláusula do 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, eu tinha uma subconsulta simples aqui, mas isso pode contornar os LIMIT
planos de consulta certos, como Feike apontou:
O planejador pode optar por gerar um plano que execute um loop aninhado sobre a LIMITing
subconsulta, causando mais do UPDATEs
que LIMIT
, por exemplo:
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
Reprodução de caso de teste
A maneira de corrigir o exposto acima foi agrupar a LIMIT
subconsulta em seu próprio CTE, pois, como o CTE é materializado, ele não retornará resultados diferentes em diferentes iterações do loop aninhado.
Ou use uma subconsulta pouco correlacionada para o caso simplesLIMIT
1
. Mais simples, mais 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;
Com acesso de gravação simultâneo
Assumindo nível de isolamento padrãoREAD COMMITTED
para tudo isso. Níveis de isolamento mais rigorosos ( REPEATABLE READ
e SERIALIZABLE
) ainda podem resultar em erros de serialização. Vejo:
Em carga de gravação simultânea, adicione FOR UPDATE SKIP LOCKED
para bloquear a linha para evitar condições de corrida. SKIP LOCKED
foi adicionado no Postgres 9.5 , para versões mais antigas, veja abaixo. O manual:
Com SKIP LOCKED
, todas as linhas selecionadas que não podem ser bloqueadas imediatamente são ignoradas. Ignorar linhas bloqueadas fornece uma visualização inconsistente dos dados; portanto, isso não é adequado para trabalhos de uso geral, mas pode ser usado para evitar contenção de bloqueios com vários consumidores acessando uma tabela semelhante a uma fila.
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;
Se não houver nenhuma linha desbloqueada qualificada, nada acontece nesta consulta (nenhuma linha é atualizada) e você obtém um resultado vazio. Para operações não críticas, significa que você terminou.
No entanto, transações simultâneas podem ter linhas bloqueadas, mas não terminam a atualização ( ROLLBACK
ou outros motivos). Para ter certeza, execute uma verificação final:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
também vê linhas bloqueadas. Enquanto isso não retorna true
, uma ou mais linhas ainda estão sendo processadas e as transações ainda podem ser revertidas. (Ou novas linhas foram adicionadas enquanto isso.) Espere um pouco e, em seguida, execute os dois passos: ( UPDATE
até que você não recupere nenhuma linha; SELECT
...) até obter true
.
Palavras-chave:
Sem SKIP LOCKED
no PostgreSQL 9.4 ou mais antigo
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;
As transações simultâneas que tentam bloquear a mesma linha são bloqueadas até a primeira liberar seu bloqueio.
Se a primeira foi revertida, a próxima transação pega o bloqueio e continua normalmente; outros na fila continuam esperando.
Se o primeiro confirmado, a WHERE
condição é reavaliada e, se não TRUE
houver mais ( status
mudou), o CTE (de maneira surpreendente) não retorna nenhuma linha. Nada acontece. Esse é o comportamento desejado quando todas as transações desejam atualizar a mesma linha .
Mas não quando cada transação deseja atualizar a próxima linha . E como queremos apenas atualizar uma linha arbitrária (ou aleatória ) , não há motivo para esperar.
Podemos desbloquear a situação com a ajuda de bloqueios consultivos :
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;
Dessa forma, a próxima linha ainda não bloqueada será atualizada. Cada transação recebe uma nova linha para trabalhar. Eu tive a ajuda do Czech Postgres Wiki para esse truque.
id
sendo qualquer bigint
coluna exclusiva (ou qualquer tipo com uma conversão implícita como int4
ou int2
).
Se bloqueios consultivos estiverem em uso para várias tabelas no seu banco de dados simultaneamente, desambigue com pg_try_advisory_xact_lock(tableoid::int, id)
- id
sendo um exclusivo integer
aqui.
Como tableoid
é uma bigint
quantidade, pode teoricamente transbordar integer
. Se você é paranóico o suficiente, use (tableoid::bigint % 2147483648)::int
- deixando uma "colisão de hash" teórica para o verdadeiro paranóico ...
Além disso, o Postgres é livre para testar WHERE
condições em qualquer ordem. Ele poderia testar pg_try_advisory_xact_lock()
e adquirir um bloqueio antes status = 'standby'
, o que poderia resultar em bloqueios consultivos adicionais em linhas não relacionadas, onde isso status = 'standby'
não é verdade. Pergunta relacionada sobre SO:
Normalmente, você pode simplesmente ignorar isso. Para garantir que apenas as linhas qualificadas estejam bloqueadas, você pode aninhar o (s) predicado (s) em um CTE como acima ou em uma subconsulta com o OFFSET 0
hack (impede o inlining) . Exemplo:
Ou (mais barato para verificações sequenciais) aninha as condições em uma CASE
declaração como:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
No entanto, o CASE
truque também impediria o Postgres de usar um índice status
. Se esse índice estiver disponível, você não precisará de aninhamento extra: apenas as linhas qualificadas serão bloqueadas em uma verificação de índice.
Como você não pode ter certeza de que um índice é usado em todas as chamadas, você pode:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
O CASE
logicamente é redundante, mas ele serve ao objetivo discutido.
Se o comando fizer parte de uma transação longa, considere bloqueios no nível da sessão que podem ser (e precisam ser) liberados manualmente. Assim, você pode desbloquear assim que terminar a linha bloqueada: pg_try_advisory_lock()
epg_advisory_unlock()
. O manual:
Uma vez adquirido no nível da sessão, um bloqueio consultivo é mantido até liberado explicitamente ou o término da sessão.
Palavras-chave: