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.
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.
Um
UPDATEcondicional e um padrao em que a precondicao fica diretamente na clausulaWHERE, 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 entaoUPDATE. 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 >= 200nao e um filtro de consulta, e uma precondicao. Se faltar saldo, a linha nao passa peloWHEREe oUPDATEsimplesmente nao muda nada. Nao ha leitura separada: o novo valor e derivado do atual (balance - 200) dentro do motor, sob um bloqueio de linha.balance = balance - 200le o valor vivo, nao o que voce viu ha um instante.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
SELECTapos oUPDATEpara saber se o debito passou. O numero de linhas modificadas ja diz. Os drivers expoem isso: no PostgreSQL viacmd_status/rowcount, e o psql mostra a tagUPDATE 1ouUPDATE 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
RETURNINGajuda: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
WHEREe voce tem compare-and-swap.UPDATE orders SET status = 'shipped' WHERE id = 42 AND status = 'paid';Apenas um move o pedido de
paidparashippede recebe 1 linha; o segundo ja nao vepaide 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 "
SELECTe depoisUPDATE" sob carga. A folga entre os passos e uma janela de corrida, e com requisicoes concorrentes o saldo fica negativo. A condicao precisa morar noWHEREdo mesmoUPDATE.READ COMMITTED(o padrao do PostgreSQL), oUPDATErele a linha antes de escrever, entao o padrao e seguro mesmo sem bloqueios explicitos. Mas se voce fez umSELECTantes e umUPDATEdepois na mesma transacao, so a condicao doUPDATEprotege, nao o que oSELECTviu.CLIENT_FOUND_ROWSo servidor retorna linhas correspondentes, senao linhas de fato alteradas. Importa quando o novo valor e igual ao antigo.ALTER TABLE ... UPDATEe 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 noWHERE, a decisao guiada pela contagem de linhas afetadas.