Sem acesso de gravação simultâneo
Materialize uma seleção em uma CTE e junte-se a ela na FROMclá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 LIMITplanos de consulta certos, como Feike apontou:
O planejador pode optar por gerar um plano que execute um loop aninhado sobre a LIMITingsubconsulta, causando mais do UPDATEsque 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 LIMITsubconsulta 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 READe SERIALIZABLE) ainda podem resultar em erros de serialização. Vejo:
Em carga de gravação simultânea, adicione FOR UPDATE SKIP LOCKEDpara bloquear a linha para evitar condições de corrida. SKIP LOCKEDfoi 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 ( ROLLBACKou outros motivos). Para ter certeza, execute uma verificação final:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECTtambé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: ( UPDATEaté que você não recupere nenhuma linha; SELECT...) até obter true.
Palavras-chave:
Sem SKIP LOCKEDno 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 WHEREcondição é reavaliada e, se não TRUEhouver mais ( statusmudou), 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.
idsendo qualquer bigintcoluna exclusiva (ou qualquer tipo com uma conversão implícita como int4ou 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)- idsendo um exclusivo integeraqui.
Como tableoidé uma bigintquantidade, 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 WHEREcondiçõ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 0hack (impede o inlining) . Exemplo:
Ou (mais barato para verificações sequenciais) aninha as condições em uma CASEdeclaração como:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
No entanto, o CASEtruque 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 CASElogicamente é 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: