Las transacciones son un concepto fundamental de todos los sistemas de bases de datos. El punto esencial de una transacción es que agrupa múltiples pasos en una única operación de todo o nada. Los estados intermedios entre los pasos no son visibles para otras transacciones concurrentes, y si ocurre algún fallo que impida que la transacción se complete, entonces ninguno de los pasos afectará a la base de datos en absoluto.
Por ejemplo, considera una base de datos bancaria que contiene saldos para varias cuentas de clientes, así como saldos de depósitos totales para las sucursales. Supón que queremos registrar un pago de $100.00 de la cuenta de Alice a la cuenta de Bob. Simplificando de manera exagerada, los comandos SQL para esto podrían verse así:
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
UPDATE branches SET balance = balance - 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice');
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Bob';
UPDATE branches SET balance = balance + 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Bob');
Los detalles de estos comandos no son importantes aquí; el punto importante es que hay varias actualizaciones separadas involucradas para lograr esta operación bastante simple. Los directivos de nuestro banco querrán asegurarse de que ocurran todas estas actualizaciones o no ocurra ninguna de ellas. Ciertamente no sería aceptable que un fallo del sistema diera como resultado que Bob recibiera $100.00 que no le fueron debitados a Alice. Tampoco Alice seguiría siendo una cliente feliz por mucho tiempo si se le debitara dinero sin acreditarse en la cuenta de Bob. Necesitamos una garantía de que si algo sale mal a mitad de la operación, ninguno de los pasos ejecutados hasta el momento tendrá efecto. Agrupar las actualizaciones en una transacción nos da esta garantía. Se dice que una transacción es atómica: desde el punto de vista de otras transacciones, ocurre por completo o no ocurre en absoluto.
También queremos una garantía de que una vez que una transacción se completa y es confirmada por el sistema de base de datos, efectivamente ha sido registrada de forma permanente y no se perderá incluso si ocurre una caída del sistema poco después. Por ejemplo, si estamos registrando un retiro de efectivo por parte de Bob, no queremos ninguna posibilidad de que el débito en su cuenta desaparezca en una caída del sistema justo después de que salga por la puerta del banco. Una base de datos transaccional garantiza que todas las actualizaciones realizadas por una transacción se registran en un almacenamiento permanente (es decir, en el disco) antes de que la transacción se informe como completada.
Otra propiedad importante de las bases de datos transaccionales está estrechamente relacionada con la noción de actualizaciones atómicas: cuando se ejecutan múltiples transacciones de forma concurrente, cada una de ellas no debería poder ver los cambios incompletos realizados por las demás. Por ejemplo, si una transacción está ocupada sumando todos los saldos de las sucursales, no sería correcto que incluyera el débito de la sucursal de Alice pero no el crédito de la sucursal de Bob, ni viceversa. Por lo tanto, las transacciones deben ser de todo o nada no solo en términos de su efecto permanente en la base de datos, sino también en términos de su visibilidad a medida que ocurren. Las actualizaciones realizadas hasta el momento por una transacción abierta son invisibles para otras transacciones hasta que la transacción se complete, momento en el cual todas las actualizaciones se vuelven visibles simultáneamente.
En PostgreSQL, una transacción se configura rodeando los comandos SQL de la
transacción con los comandos BEGIN y COMMIT. Así que nuestra
transacción bancaria en realidad se vería así:
BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
-- etc etc
COMMIT;
Si a mitad de la transacción decidimos que no queremos confirmar los cambios (tal vez acabamos de notar
que el saldo de Alice quedó en negativo), podemos emitir el comando ROLLBACK en lugar
de COMMIT, y todas nuestras actualizaciones hasta el momento serán canceladas.
De hecho, PostgreSQL trata cada sentencia SQL como si se ejecutara dentro de
una transacción. Si no emites un comando BEGIN, entonces cada sentencia de forma
individual tiene un BEGIN implícito y (si tiene éxito) un COMMIT
que la envuelve. Un grupo de sentencias rodeado por BEGIN y COMMIT
se denomina a veces bloque de transacción.
Algunas librerías cliente emiten comandos BEGIN y COMMIT
automáticamente, por lo que podrías obtener el efecto de bloques de transacciones sin solicitarlo.
Consulta la documentación de la interfaz que estés utilizando.
Es posible controlar las sentencias de una transacción de una manera más granular mediante el uso de
puntos de salvaguarda (savepoints). Los puntos de salvaguarda te permiten descartar
selectivamente partes de la transacción, mientras confirmas el resto. Después de definir un punto de
salvaguarda con SAVEPOINT, puedes, si es necesario, revertir los cambios hasta el punto
de salvaguarda con ROLLBACK TO. Todos los cambios de la base de datos de la transacción
entre la definición del punto de salvaguarda y la reversión a este se descartan, pero se conservan los cambios
anteriores al punto de salvaguarda.
Después de revertir los cambios a un punto de salvaguarda, este sigue estando definido, por lo que puedes revertir a él varias veces. Por el contrario, si estás seguro de que no necesitarás revertir a un punto de salvaguarda en particular nuevamente, este se puede liberar, de modo que el sistema pueda liberar algunos recursos. Ten en cuenta que tanto liberar como revertir a un punto de salvaguarda liberará automáticamente todos los puntos de salvaguarda que se definieron después de él.
Todo esto ocurre dentro del bloque de transacción, por lo que nada de esto es visible para otras sesiones de la base de datos. Cuando confirmes (hagas commit) el bloque de transacción, las acciones confirmadas se volverán visibles como una unidad para otras sesiones, mientras que las acciones revertidas nunca se volverán visibles en absoluto.
Recordando la base de datos del banco, supón que debitamos $100.00 de la cuenta de Alice y acreditamos la cuenta de Bob, solo para descubrir más tarde que deberíamos haber acreditado la cuenta de Wally. Podríamos hacerlo usando puntos de salvaguarda de esta manera:
BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Bob';
-- oops ... forget that and use Wally's account
ROLLBACK TO my_savepoint;
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Wally';
COMMIT;
Este ejemplo está, por supuesto, simplificado en exceso, pero es posible tener mucho control en un bloque
de transacción mediante el uso de puntos de salvaguarda. Además, ROLLBACK TO es la única
forma de recuperar el control de un bloque de transacción que el sistema puso en estado abortado debido a un
error, sin tener que revertirlo por completo y comenzar de nuevo.