sqlpostgresqlupdateconcurrency

UPDATE condicional no SQL: verificacao e escrita atomica com WHERE

Codifique uma precondicao no WHERE de um unico UPDATE, detecte a falha pela contagem de linhas afetadas e evite updates perdidos: compare-and-swap no SQL.

3 min de leituraReferencesql · postgresql · update · concurrency · transactions · mysql

Um UPDATE condicional e um padrao em que a precondicao fica diretamente na clausula WHERE, de modo que a verificacao e a escrita acontecem em uma unica instrucao atomica. O caso classico e debitar de uma conta apenas se houver saldo: o proprio banco decide que a linha nao se qualifica e relata zero linhas afetadas em vez de corromper dados.

Verificar e escrever em uma instrucao

Pegue uma conta e tente debitar 200. A abordagem ingenua e SELECT balance, comparar no codigo e entao UPDATE. Entre esses passos outra transacao pode mudar o saldo, e voce gasta dinheiro que ja nao existe. O certo e fundir a verificacao com a escrita:

UPDATE accounts
SET balance = balance - 200
WHERE id = 1
  AND balance >= 200;

Aqui balance >= 200 nao e um filtro de consulta, e uma precondicao. Se faltar saldo, a linha nao passa pelo WHERE e o UPDATE simplesmente nao muda nada. Nao ha leitura separada: o novo valor e derivado do atual (balance - 200) dentro do motor, sob um bloqueio de linha.

  • A condicao e a escrita veem a mesma versao da linha; nada pode entrar no meio.
  • balance = balance - 200 le o valor vivo, nao o que voce viu ha um instante.
  • Se nao existir nenhuma linha com id = 1, o resultado e o mesmo: zero linhas afetadas.

A contagem de linhas afetadas como sinal

O truque central e nao rodar um segundo SELECT apos o UPDATE para saber se o debito passou. O numero de linhas modificadas ja diz. Os drivers expoem isso: no PostgreSQL via cmd_status / rowcount, e o psql mostra a tag UPDATE 1 ou UPDATE 0.

-- 1 linha -> debito feito; 0 linhas -> insufficient funds
UPDATE accounts
SET balance = balance - 200
WHERE id = 1
  AND balance >= 200;

A logica da aplicacao fica trivial: recebeu 1, sucesso; recebeu 0, "saldo insuficiente" (ou conta inexistente). Para separar esses dois casos, o RETURNING ajuda:

UPDATE accounts
SET balance = balance - 200
WHERE id = 1
  AND balance >= 200
RETURNING id, balance;

Um resultado vazio e uma recusa; uma unica linha e sucesso com o novo saldo, sem consulta extra.

Compare-and-swap por versao

O mesmo padrao resolve o problema de update perdido para qualquer campo, nao so para dinheiro. Suponha que dois processos leem um pedido e ambos querem altera-lo. Coloque o valor anterior esperado no WHERE e voce tem compare-and-swap.

UPDATE orders
SET status = 'shipped'
WHERE id = 42
  AND status = 'paid';

Apenas um move o pedido de paid para shipped e recebe 1 linha; o segundo ja nao ve paid e recebe 0, seu update e recusado com seguranca. Muitas vezes uma coluna de versao sustenta isso:

UPDATE orders
SET status = 'shipped',
    version = version + 1
WHERE id = 42
  AND version = 7;

Isso e bloqueio otimista: voce nao mantem a linha travada enquanto pensa, apenas verifica no momento da escrita que ninguem a tocou.

Uma armadilha e diferencas entre bancos

O grande erro e voltar ao esquema de dois passos "SELECT e depois UPDATE" sob carga. A folga entre os passos e uma janela de corrida, e com requisicoes concorrentes o saldo fica negativo. A condicao precisa morar no WHERE do mesmo UPDATE.

  • Transacoes. Em READ COMMITTED (o padrao do PostgreSQL), o UPDATE rele a linha antes de escrever, entao o padrao e seguro mesmo sem bloqueios explicitos. Mas se voce fez um SELECT antes e um UPDATE depois na mesma transacao, so a condicao do UPDATE protege, nao o que o SELECT viu.
  • MySQL/InnoDB. Mesmo comportamento; a contagem depende de uma flag: com CLIENT_FOUND_ROWS o servidor retorna linhas correspondentes, senao linhas de fato alteradas. Importa quando o novo valor e igual ao antigo.
  • ClickHouse. ALTER TABLE ... UPDATE e assincrono e nao transacional, e nao garante verificacoes como "nunca ficar negativo". ClickHouse nao foi feito para check-and-set estrito; mantenha esse saldo em um banco OLTP.

Lembre-se: um unico UPDATE, a precondicao no WHERE, a decisao guiada pela contagem de linhas afetadas.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador