Transacciones en SQL Server

Una transacción es un conjunto de operaciones que se tratan como una única unidad: o se completan todas o no se completa ninguna. Esta garantía es lo que permite mantener la integridad de los datos incluso cuando algo falla a mitad de un proceso. SQL Server lleva soportando transacciones explícitas desde sus primeras versiones, aunque la forma de escribirlas correctamente ha evolucionado bastante desde aquellos años.

Propiedades ACID

Cualquier sistema gestor de bases de datos que soporte transacciones debe garantizar las cuatro propiedades ACID:

  • Atomicidad: todas las operaciones de la transacción se ejecutan o ninguna lo hace.
  • Consistencia: la transacción lleva la base de datos de un estado válido a otro estado válido.
  • Aislamiento: las operaciones de una transacción no son visibles para otras transacciones hasta que se confirman.
  • Durabilidad: una vez confirmada, la transacción persiste aunque el sistema falle inmediatamente después.

Transacción autocommit

La transacción más sencilla en SQL Server es una sola sentencia. Cuando ejecutas algo como esto:

UPDATE Products SET UnitPrice = 20 WHERE ProductName = 'Chai'

SQL Server la envuelve automáticamente en una transacción. Si algo falla durante la escritura, el motor puede leer el log de transacciones y deshacer el cambio. Este modo se llama autocommit y no requiere intervención. El problema aparece cuando necesitas agrupar varias operaciones para que se traten como una sola.

Sentencias básicas: BEGIN, COMMIT y ROLLBACK

Para definir una transacción explícita se usan tres sentencias:

  • BEGIN TRAN — marca el inicio de la transacción.
  • COMMIT TRAN — confirma los cambios y los hace permanentes.
  • ROLLBACK TRAN — deshace todos los cambios desde el BEGIN TRAN.

Manejo de errores: el patrón moderno con TRY/CATCH

El código de los años 90 usaba GOTO para capturar errores, un patrón que funcionaba pero resultaba difícil de leer y mantener. Desde SQL Server 2005 existe el bloque TRY/CATCH, que es la forma correcta de gestionar errores en transacciones:

BEGIN TRY
    BEGIN TRAN

    UPDATE Products SET UnitPrice = 20 WHERE ProductName = 'Chai'
    UPDATE Products SET UnitPrice = 18 WHERE ProductName = 'Chang'

    COMMIT TRAN
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRAN

    -- Relanzar el error con información útil
    THROW
END CATCH

Dentro del bloque CATCH tienes acceso a varias funciones que describen el error producido:

  • ERROR_NUMBER() — número de error de SQL Server.
  • ERROR_MESSAGE() — texto descriptivo del error.
  • ERROR_LINE() — línea donde se produjo el error.
  • ERROR_PROCEDURE() — nombre del procedimiento almacenado donde ocurrió, si aplica.
  • ERROR_SEVERITY() y ERROR_STATE() — gravedad y estado del error.
BEGIN TRY
    BEGIN TRAN
    INSERT INTO Pedidos (id_cliente, fecha) VALUES (42, GETDATE())
    INSERT INTO LineaDetalle (id_pedido, id_producto, cantidad) VALUES (SCOPE_IDENTITY(), 7, 3)
    COMMIT TRAN
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRAN

    SELECT
        ERROR_NUMBER()    AS numero,
        ERROR_MESSAGE()   AS mensaje,
        ERROR_LINE()      AS linea,
        ERROR_SEVERITY()  AS gravedad
END CATCH

SET XACT_ABORT ON

Una opción muy útil es activar XACT_ABORT al inicio del batch o procedimiento almacenado. Cuando está activada, cualquier error en tiempo de ejecución cancela automáticamente la transacción en curso, sin necesidad de comprobar @@ERROR después de cada sentencia:

SET XACT_ABORT ON

BEGIN TRAN
    UPDATE Clientes SET credito = credito - 500 WHERE id = 1
    UPDATE Cuentas  SET saldo  = saldo   + 500 WHERE id = 99
COMMIT TRAN
-- Si la segunda UPDATE falla, la transacción se deshace automáticamente

El uso conjunto de TRY/CATCH y SET XACT_ABORT ON es el patrón recomendado hoy para transacciones en SQL Server.

Transacciones anidadas y @@TRANCOUNT

SQL Server permite anidar transacciones: puedes abrir una nueva transacción sin haber cerrado la anterior. La variable del sistema @@TRANCOUNT lleva la cuenta del nivel de anidamiento: vale 0 cuando no hay ninguna transacción abierta, 1 cuando hay una, 2 cuando hay dos niveles, y así sucesivamente.

El comportamiento de COMMIT y ROLLBACK en este contexto es asimétrico, y es importante entenderlo bien:

  • COMMIT TRAN en una transacción anidada solo reduce @@TRANCOUNT en 1. No confirma nada en la base de datos hasta que @@TRANCOUNT llega a 0.
  • ROLLBACK TRAN sin nombre deshace todas las transacciones, hasta la más externa, y pone @@TRANCOUNT a 0 de golpe.
CREATE TABLE Test (Columna int)
GO

BEGIN TRAN TranExterna        -- @@TRANCOUNT = 1
    INSERT INTO Test VALUES (1)
    BEGIN TRAN TranInterna    -- @@TRANCOUNT = 2
        INSERT INTO Test VALUES (2)
    COMMIT TRAN TranInterna   -- @@TRANCOUNT = 1, pero aún no se guarda nada
COMMIT TRAN TranExterna       -- @@TRANCOUNT = 0, ahora sí se persisten los datos

SELECT * FROM Test
-- Resultado: 1, 2

Con ROLLBACK en anidamiento:

BEGIN TRAN TranExterna        -- @@TRANCOUNT = 1
    INSERT INTO Test VALUES (1)
    BEGIN TRAN TranInterna    -- @@TRANCOUNT = 2
        INSERT INTO Test VALUES (2)
    ROLLBACK TRAN             -- @@TRANCOUNT = 0, se deshace TODO
SELECT * FROM Test
-- Resultado: tabla vacía

SAVE TRAN: puntos de restauración parciales

La sentencia SAVE TRAN permite crear un punto de restauración dentro de una transacción. Un ROLLBACK hasta ese punto deshace solo lo ocurrido desde el SAVE, sin cancelar la transacción ni modificar @@TRANCOUNT:

CREATE TABLE Tabla1 (Columna1 varchar(50))
GO

BEGIN TRAN
    INSERT INTO Tabla1 VALUES ('Primer valor')
    SAVE TRAN Punto1
    INSERT INTO Tabla1 VALUES ('Segundo valor')
    ROLLBACK TRAN Punto1       -- deshace solo el segundo INSERT
    INSERT INTO Tabla1 VALUES ('Tercer valor')
COMMIT TRAN

SELECT * FROM Tabla1
-- Resultado: 'Primer valor', 'Tercer valor'

El ROLLBACK al savepoint no afecta a @@TRANCOUNT, lo que lo hace útil dentro de procedimientos almacenados que pueden ser llamados desde una transacción externa.

Transacciones en procedimientos almacenados

Cuando usas ROLLBACK TRAN dentro de un procedimiento almacenado, deshace no solo la transacción interna sino también la transacción externa desde la que se llamó al procedimiento. Esto genera errores difíciles de diagnosticar si no se tiene en cuenta.

El patrón correcto combina SAVE TRAN con TRY/CATCH para que el procedimiento pueda deshacer su propio trabajo sin afectar a la transacción del llamador:

CREATE PROCEDURE dbo.InsertaDetalle
    @id_pedido  INT,
    @id_producto INT,
    @cantidad   INT
AS
SET XACT_ABORT OFF  -- controlamos el rollback manualmente

DECLARE @savepoint NVARCHAR(32) = N'SP_' + CAST(@@TRANCOUNT AS NVARCHAR)

BEGIN TRY
    IF @@TRANCOUNT = 0
        BEGIN TRAN
    ELSE
        SAVE TRAN @savepoint

    INSERT INTO LineaDetalle (id_pedido, id_producto, cantidad)
    VALUES (@id_pedido, @id_producto, @cantidad)

    IF @@TRANCOUNT > 0 AND XACT_STATE() = 1
        COMMIT TRAN

END TRY
BEGIN CATCH
    IF XACT_STATE() = -1
        ROLLBACK TRAN               -- transacción no confirmable: rollback total
    ELSE IF XACT_STATE() = 1
        ROLLBACK TRAN @savepoint    -- solo deshacemos hasta nuestro savepoint

    THROW  -- propagamos el error al llamador
END CATCH
GO

XACT_STATE() es la función que permite saber en qué estado está la transacción actual: devuelve 1 si está activa y es confirmable, -1 si está activa pero ya no se puede confirmar (solo se puede hacer ROLLBACK), y 0 si no hay transacción abierta.

Resumen rápido

Sentencia

Qué hace

Efecto en @@TRANCOUNT

BEGIN TRAN

Abre una transacción

+1

COMMIT TRAN

Confirma (o reduce nivel)

-1 (datos al disco solo si llega a 0)

ROLLBACK TRAN

Deshace hasta el BEGIN más externo

? 0

SAVE TRAN nombre

Crea punto de restauración

Sin cambio

ROLLBACK TRAN nombre

Deshace hasta el savepoint

Sin cambio

Para más detalle sobre las sentencias de control de transacciones puedes consultar la documentación oficial actualizada de Microsoft Learn: Transactions (Transact-SQL) y la referencia de TRY/CATCH (Transact-SQL).

Si quieres profundizar en SQL más allá de las transacciones, el artículo SQL: el lenguaje universal para el manejo de bases de datos relacionales es un buen punto de partida, y en bases de datos emergentes en 2025 puedes ver qué alternativas a SQL Server están ganando terreno.

Imagen: Pexels / Kevin Ku

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP