Un UPDATE condicional es un patron en el que la precondicion vive directamente en la clausula WHERE, de modo que la comprobacion y la escritura ocurren en una sola sentencia atomica. El caso de manual es descontar de una cuenta solo si hay saldo: la propia base decide que la fila no califica y reporta cero filas afectadas en lugar de corromper datos.
Comprobar y escribir en una sentencia
Tomemos una cuenta e intentemos descontar 200. El enfoque ingenuo es SELECT balance, comparar en el codigo y luego UPDATE. Entre esos pasos otra transaccion puede cambiar el saldo, y gastas dinero que ya no existe. Lo correcto es fundir la comprobacion con la escritura:
UPDATE accounts
SET balance = balance - 200
WHERE id = 1
AND balance >= 200;
Aqui balance >= 200 no es un filtro de consulta, es una precondicion. Si falta saldo, la fila no pasa el WHERE y el UPDATE simplemente no cambia nada. No hay lectura aparte: el nuevo valor se deriva del actual (balance - 200) dentro del motor, bajo un bloqueo de fila.
- La condicion y la escritura ven la misma version de la fila; nada puede colarse en medio.
balance = balance - 200 lee el valor vivo, no el que viste hace un instante.
- Si no existe ninguna fila con
id = 1, el resultado es el mismo: cero filas afectadas.
El conteo de filas afectadas como senal
El truco clave es no lanzar un segundo SELECT tras el UPDATE para saber si el cargo se aplico. El numero de filas modificadas ya lo dice. Los drivers lo exponen: en PostgreSQL via cmd_status / rowcount, y psql muestra la etiqueta UPDATE 1 o UPDATE 0.
UPDATE accounts
SET balance = balance - 200
WHERE id = 1
AND balance >= 200;
La logica de la aplicacion se vuelve trivial: si recibes 1, exito; si recibes 0, "saldo insuficiente" (o no existe la cuenta). Para distinguir esos dos casos, RETURNING resulta comodo:
UPDATE accounts
SET balance = balance - 200
WHERE id = 1
AND balance >= 200
RETURNING id, balance;
Un resultado vacio es un rechazo; una sola fila es exito con el nuevo saldo, sin consulta adicional.
Compare-and-swap por version
El mismo patron resuelve la perdida de actualizaciones para cualquier campo, no solo el dinero. Supon que dos procesos leen un pedido y ambos quieren cambiarlo. Pon el valor previo esperado en el WHERE y tienes compare-and-swap.
UPDATE orders
SET status = 'shipped'
WHERE id = 42
AND status = 'paid';
Solo uno pasa el pedido de paid a shipped y obtiene 1 fila; el segundo ya no ve paid y obtiene 0, su actualizacion queda rechazada de forma segura. A menudo lo respalda una columna de version:
UPDATE orders
SET status = 'shipped',
version = version + 1
WHERE id = 42
AND version = 7;
Esto es bloqueo optimista: no mantienes la fila bloqueada mientras decides, solo verificas en el momento de escribir que nadie la toco.
Una trampa y diferencias entre bases
El gran error es volver al esquema de dos pasos "SELECT y luego UPDATE" bajo carga. El hueco entre los pasos es una ventana de carrera, y con peticiones concurrentes el saldo se va a negativo. La condicion debe vivir en el WHERE del mismo UPDATE.
- Transacciones. En
READ COMMITTED (el valor por defecto de PostgreSQL), el UPDATE relee la fila antes de escribir, asi que el patron es seguro incluso sin bloqueos explicitos. Pero si hiciste un SELECT antes y un UPDATE despues en una transaccion, solo te protege la condicion del UPDATE, no lo que vio el SELECT.
- MySQL/InnoDB. Igual comportamiento; el conteo depende de un flag: con
CLIENT_FOUND_ROWS el servidor devuelve filas coincidentes, si no, filas realmente modificadas. Importa cuando el nuevo valor es igual al anterior.
- ClickHouse.
ALTER TABLE ... UPDATE es asincrono y no transaccional, y no garantiza comprobaciones como "nunca quedar en negativo". ClickHouse no esta hecho para un check-and-set estricto; manten ese saldo en una base OLTP.
Recuerda: un solo UPDATE, la precondicion en el WHERE, la decision guiada por el conteo de filas afectadas.
Un
UPDATEcondicional es un patron en el que la precondicion vive directamente en la clausulaWHERE, de modo que la comprobacion y la escritura ocurren en una sola sentencia atomica. El caso de manual es descontar de una cuenta solo si hay saldo: la propia base decide que la fila no califica y reporta cero filas afectadas en lugar de corromper datos.Comprobar y escribir en una sentencia
Tomemos una cuenta e intentemos descontar 200. El enfoque ingenuo es
SELECT balance, comparar en el codigo y luegoUPDATE. Entre esos pasos otra transaccion puede cambiar el saldo, y gastas dinero que ya no existe. Lo correcto es fundir la comprobacion con la escritura:UPDATE accounts SET balance = balance - 200 WHERE id = 1 AND balance >= 200;Aqui
balance >= 200no es un filtro de consulta, es una precondicion. Si falta saldo, la fila no pasa elWHEREy elUPDATEsimplemente no cambia nada. No hay lectura aparte: el nuevo valor se deriva del actual (balance - 200) dentro del motor, bajo un bloqueo de fila.balance = balance - 200lee el valor vivo, no el que viste hace un instante.id = 1, el resultado es el mismo: cero filas afectadas.El conteo de filas afectadas como senal
El truco clave es no lanzar un segundo
SELECTtras elUPDATEpara saber si el cargo se aplico. El numero de filas modificadas ya lo dice. Los drivers lo exponen: en PostgreSQL viacmd_status/rowcount, y psql muestra la etiquetaUPDATE 1oUPDATE 0.-- 1 fila -> cargo aplicado; 0 filas -> insufficient funds UPDATE accounts SET balance = balance - 200 WHERE id = 1 AND balance >= 200;La logica de la aplicacion se vuelve trivial: si recibes 1, exito; si recibes 0, "saldo insuficiente" (o no existe la cuenta). Para distinguir esos dos casos,
RETURNINGresulta comodo:UPDATE accounts SET balance = balance - 200 WHERE id = 1 AND balance >= 200 RETURNING id, balance;Un resultado vacio es un rechazo; una sola fila es exito con el nuevo saldo, sin consulta adicional.
Compare-and-swap por version
El mismo patron resuelve la perdida de actualizaciones para cualquier campo, no solo el dinero. Supon que dos procesos leen un pedido y ambos quieren cambiarlo. Pon el valor previo esperado en el
WHEREy tienes compare-and-swap.UPDATE orders SET status = 'shipped' WHERE id = 42 AND status = 'paid';Solo uno pasa el pedido de
paidashippedy obtiene 1 fila; el segundo ya no vepaidy obtiene 0, su actualizacion queda rechazada de forma segura. A menudo lo respalda una columna de version:UPDATE orders SET status = 'shipped', version = version + 1 WHERE id = 42 AND version = 7;Esto es bloqueo optimista: no mantienes la fila bloqueada mientras decides, solo verificas en el momento de escribir que nadie la toco.
Una trampa y diferencias entre bases
El gran error es volver al esquema de dos pasos "
SELECTy luegoUPDATE" bajo carga. El hueco entre los pasos es una ventana de carrera, y con peticiones concurrentes el saldo se va a negativo. La condicion debe vivir en elWHEREdel mismoUPDATE.READ COMMITTED(el valor por defecto de PostgreSQL), elUPDATErelee la fila antes de escribir, asi que el patron es seguro incluso sin bloqueos explicitos. Pero si hiciste unSELECTantes y unUPDATEdespues en una transaccion, solo te protege la condicion delUPDATE, no lo que vio elSELECT.CLIENT_FOUND_ROWSel servidor devuelve filas coincidentes, si no, filas realmente modificadas. Importa cuando el nuevo valor es igual al anterior.ALTER TABLE ... UPDATEes asincrono y no transaccional, y no garantiza comprobaciones como "nunca quedar en negativo". ClickHouse no esta hecho para un check-and-set estricto; manten ese saldo en una base OLTP.Recuerda: un solo
UPDATE, la precondicion en elWHERE, la decision guiada por el conteo de filas afectadas.