PostgreSQL
Transacciones y ACID
Transacciones en PostgreSQL, propiedades ACID, BEGIN, COMMIT, ROLLBACK, savepoints y límites para mantener operaciones consistentes.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
PostgreSQL
Transacciones en PostgreSQL, propiedades ACID, BEGIN, COMMIT, ROLLBACK, savepoints y límites para mantener operaciones consistentes.
Una transacción agrupa sentencias que representan una sola intención lógica:
estado válido inicial
↓
varias lecturas y escrituras coordinadas
↓
COMMIT: nuevo estado visible
o
ROLLBACK: efectos locales descartadosEjemplo: crear una orden, sus items, reservar stock y registrar un evento outbox deben ocurrir como una unidad. Si falla cualquiera de esas operaciones, la base no debe conservar una orden incompleta.
Sin una transacción explícita, cada sentencia funciona normalmente en autocommit:
INSERT order → confirma
INSERT items → falla
UPDATE stock → nunca ocurreEl resultado es una order sin items. La aplicación puede intentar compensarlo, pero la base ya quedó en un estado parcial.
Una transacción evita este tipo de atomicidad parcial dentro de PostgreSQL.
BEGIN;
INSERT INTO orders (customer_id, status)
VALUES ($1, 'pending')
RETURNING id;
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES ($2, $3, $4, $5);
UPDATE inventory
SET quantity = quantity - $4
WHERE branch_id = $6
AND product_id = $3
AND quantity >= $4;
COMMIT;Flujo:
BEGIN abre el bloque transaccional en esa sesión.COMMIT hace durables y visibles los cambios según configuración.ROLLBACK descarta cambios de esa transacción.Todas las sentencias deben ejecutarse en la misma conexión. Una transacción pertenece a una sesión PostgreSQL, no al pool abstracto de la aplicación.
Cuando no existe un bloque explícito, PostgreSQL ejecuta cada statement dentro de su propia transacción:
UPDATE products SET price = 100 WHERE id = 1;Aunque no escribas BEGIN, la sentencia sigue siendo atómica. El problema aparece cuando una intención necesita varias sentencias.
Todas las modificaciones locales de la transacción confirman o ninguna.
No significa que un email, una llamada HTTP o un archivo externo también se reviertan.
La transacción lleva la base desde un estado válido hacia otro, siempre que constraints y lógica sean correctos.
PostgreSQL no conoce por sí mismo reglas no expresadas. ACID no vuelve correcto un modelo incorrecto.
Controla qué efectos concurrentes puede observar una transacción. No significa ausencia total de concurrencia ni bloqueos.
Después de que COMMIT responde exitosamente, PostgreSQL utiliza WAL, fsync y almacenamiento para preservar el cambio ante fallos dentro de las garantías configuradas.
Parámetros como synchronous_commit, hardware defectuoso, pérdida completa de región o backups inexistentes cambian el riesgo. Durabilidad no significa “imposible perder datos bajo cualquier desastre”.
Si una sentencia produce error dentro de una transacción:
BEGIN;
INSERT ...;
-- error
SELECT 1;La sesión responde con un error similar a “current transaction is aborted”. Debes ejecutar:
ROLLBACK;o volver a un savepoint válido.
Continuar enviando queries sin rollback mantiene la conexión ocupada e inutilizable para ese trabajo.
BEGIN;
INSERT INTO orders (...) RETURNING id;
SAVEPOINT before_optional_coupon;
UPDATE coupons
SET uses = uses + 1
WHERE code = $1;
-- si la operación opcional falla
ROLLBACK TO SAVEPOINT before_optional_coupon;
COMMIT;Un savepoint permite deshacer una parte sin abandonar toda la transacción.
Costes:
La frontera debe coincidir con la unidad que necesita consistencia.
Ejemplo apropiado:
crear order
+ crear items
+ reservar inventory
+ insertar outbox event
= una transacciónEjemplo excesivo:
BEGIN
→ consultar catálogo
→ llamar API de pagos 20 s
→ esperar respuesta
→ enviar email
→ COMMITEsto retiene:
PostgreSQL no puede revertir un pago procesado por otro sistema.
Patrón peligroso:
cobrar proveedor
→ INSERT local fallaPatrón habitual:
transacción local:
registrar payment_pending
registrar outbox event
COMMIT
↓
worker ejecuta cobro idempotente
↓
actualiza resultado en otra transacciónEsto introduce consistencia eventual, pero evita mantener locks mientras depende de red y permite retries controlados.
BEGIN;
INSERT INTO orders (...) RETURNING id;
INSERT INTO outbox_events (
aggregate_type,
aggregate_id,
event_type,
payload
)
VALUES ('order', $1, 'order.created', $2::jsonb);
COMMIT;La order y el evento se confirman juntos. Un worker publica el evento después.
La outbox no garantiza entrega exactamente una vez por sí sola; el consumidor debe ser idempotente.
Una transacción larga puede:
Evita idle in transaction: una sesión abrió transacción y quedó esperando sin hacer trabajo.
Configura límites como:
SET LOCAL statement_timeout = '5s';
SET LOCAL lock_timeout = '1s';
SET LOCAL idle_in_transaction_session_timeout = '30s';Los valores dependen del workload.
Una sentencia puede completar sin error y afectar cero filas:
UPDATE inventory
SET quantity = quantity - $1
WHERE branch_id = $2
AND product_id = $3
AND quantity >= $1
RETURNING quantity;Antes de COMMIT, la aplicación debe comprobar que obtuvo una fila. Cero filas puede significar stock insuficiente o recurso inexistente.
COMMIT no valida por sí mismo la intención del negocio.
PostgreSQL puede abortar transacciones por:
40001: serialization failure.40P01: deadlock detected.El retry debe envolver toda la función transaccional:
intento 1:
BEGIN → todas las operaciones → error 40001 → ROLLBACK
intento 2:
BEGIN → repetir lecturas y escrituras → COMMITNo reintentes únicamente la sentencia final, porque las decisiones anteriores se tomaron con un snapshot viejo.
El retry necesita:
PostgreSQL permite revertir muchas operaciones DDL:
BEGIN;
CREATE TABLE example (...);
ROLLBACK;Pero existen excepciones y restricciones:
CREATE INDEX CONCURRENTLY no puede ejecutarse en un transaction block.“Se puede revertir” no significa “es seguro ejecutarlo en producción sin plan”.
BEGIN TRANSACTION
ISOLATION LEVEL SERIALIZABLE
READ ONLY
DEFERRABLE;Una transacción serializable read-only deferrable puede esperar al inicio hasta obtener un snapshot seguro, reduciendo riesgo de serialization failures en ciertos reportes largos.
Es una herramienta especializada, no un default universal.
BEGIN;
-- cambios
PREPARE TRANSACTION 'order-123';
COMMIT PREPARED 'order-123';Permite que un coordinador decida después entre commit y rollback.
Riesgos:
max_prepared_transactions.No lo uses como sustituto casual de outbox o saga.
BEGIN;
SELECT status
FROM orders
WHERE id = $1
FOR UPDATE;
UPDATE orders
SET status = 'confirmed',
confirmed_at = now()
WHERE id = $1
AND status = 'pending';
UPDATE inventory i
SET reserved_quantity = reserved_quantity + oi.quantity
FROM order_items oi
WHERE oi.order_id = $1
AND i.branch_id = $2
AND i.product_id = oi.product_id
AND i.quantity - i.reserved_quantity >= oi.quantity;
-- verificar que se reservaron todos los items
INSERT INTO outbox_events (...);
COMMIT;La implementación real debe comprobar:
Un error en cualquier paso debe producir rollback.
La transacción pudo confirmar aunque el cliente no recibiera respuesta. Reintentar ciegamente puede duplicar la operación. Usa idempotency key y consulta el resultado.
La aplicación puede no saber si el servidor confirmó. Trata el resultado como incierto y reconcilia mediante identidad estable.
Es válida, pero normalmente innecesaria.
Solo la parte posterior al savepoint se revierte; revisa variables y efectos locales de aplicación.
La transacción entra en estado abortado para esa sentencia; necesita rollback o savepoint.
COMMIT puede ejecutarse fuera de la transacción o fallar. Usa un client reservado.
Retiene recursos mientras se valida input o llama servicios.
Confirma una operación incompleta.
Puede repetir fallos permanentes o side effects.
No puede revertirse y prolonga el bloque.
Impide vacuum y agota pool.
La conexión continúa abortada.
La transacción confirma datos inválidos si no existe una regla que los impida.
Una operación set-based puede ser atómica por sí sola:
UPDATE inventory
SET quantity = quantity - $1
WHERE id = $2 AND quantity >= $1
RETURNING quantity;No añadas un bloque explícito si no coordina trabajo adicional.
COMMIT exitoso no elimina la necesidad de backups y DR.BEGIN y COMMIT deben usar el mismo client?MVCC, snapshots y visibilidad explica cómo PostgreSQL permite que transacciones concurrentes observen versiones distintas de una misma fila.