No Postgres 13, tenho uma tabela que é atualizada com frequência. No entanto, a consulta de atualização é bastante complicada e usa os mesmos valores várias vezes. Então, usar um CTE parece uma coisa bastante lógica a se fazer.
Um exemplo simplificado se parece com isso:
WITH my_cte AS (
SELECT
my_id,
CASE WHEN my_value1 > 100 THEN 50 ELSE 10 END AS my_addition
FROM my_table
WHERE my_id = $1
)
UPDATE my_table
SET my_value1 = my_table.my_value1 + my_cte.my_addition,
my_value2 = my_table.my_value2 + my_cte.my_addition
FROM my_cte
WHERE my_table.my_id = my_cte.my_id
Agora estou me perguntando: O que aconteceria se entre o SELECT
no CTE e o UPDATE
, a tabela fosse atualizada por outra consulta, alterando my_value1
assim, o cálculo de my_addition
ficasse desatualizado e errado quando UPDATE
isso acontecesse. Tal situação pode ocorrer? Ou o Postgres define um bloqueio implícito automaticamente?
Se o Postgres não faz mágica aqui e eu mesmo preciso cuidar disso: seria suficiente fazer FOR UPDATE
no SELECT
CTE?
Desculpe se não fui claro aqui: Não é que eu queira "ver" essas modificações simultâneas, eu quero evitá-las, ou seja, uma vez que o cálculo SELECT
é feito, nenhuma outra consulta pode modificar essa mesma linha até que UPDATE
seja feito.
Na vida real, o que eu zombei aqui CASE WHEN my_value1 > 100 THEN 50 ELSE 10 END
tem cerca de 20 linhas e eu preciso disso em cerca de 5 lugares no arquivo UPDATE
. Como sou um grande fã de "Não se repita", acho que um CTE é o caminho a percorrer. Ou existe uma maneira melhor de evitar copiar e colar em um UPDATE
sem CTE?
O Postgres usa um modelo multiversão ( Multiversion Concurrency Control, MVCC ).
READ COMMITTED
No nível de isolamento padrão , cada consulta separada efetivamente vê um instantâneo do banco de dados a partir do instante em que a consulta começa a ser executada. Consultas subsequentes - mesmo dentro da mesma transação - podem ver um instantâneo diferente se transações simultâneas forem confirmadas no meio. (Além do que foi feito na mesma transação até agora.)No entanto, no que diz respeito às CTEs , todas as sub-instruções em
WITH
são executadas simultaneamente com a instrução externa, elas efetivamente veem o mesmo instantâneo do banco de dados. Tudo isso é considerado uma única consulta para esse fim.Portanto, não , você não precisa de um bloqueio explícito para permanecer consistente.
Encapsular a lógica em uma função pode ser conveniente por vários motivos, mas isso não tem nenhum efeito sobre a concorrência. Além: um CTE com uma função volátil nunca é embutido. Ver:
A
SELECT
não bloqueia as linhas consultadas. O Postgres permite arquivosUPDATES
. MasUPDATE
bloqueia as linhas de destino. As transações simultâneas que tentam gravar também precisam esperar até que a transação de bloqueio seja concluída.Se você quiser proibir gravações em linhas (colunas) que foram selecionadas apenas enquanto o seu
UPDATE
está em andamento, você pode querer fazer bloqueios de qualquer maneira (ou usar um nível de isolamento mais estrito). TalvezFOR UPDATE
fechaduras, ou talvez uma fechadura mais fraca . Isso depende dos detalhes e requisitos que você está expressamente retendo / não fornecendo em sua pergunta.Além disso (embora você não tenha solicitado isso), se várias transações simultâneas puderem estar gravando em linhas sobrepostas (mais de uma por vez), certifique-se de seguir a mesma ordem consistente de linhas para evitar deadlocks .
Se você deseja impedir que instruções simultâneas modifiquem as linhas que o CTE seleciona antes de serem atualizadas, você precisa usar
SELECT ... FOR NO KEY UPDATE
no CTE.Com base no que a_horse_with_no_name disse:
Coloque a lógica de adição em uma função e chame essa função toda vez que for definir um novo valor. Isso irá ajudá-lo de duas maneiras.
Algo assim deve funcionar.