SAVEPOINT

Un SAVEPOINT crea un punto de restauración dentro de una transacción. Permite revertir parte de los cambios sin cancelar toda la transacción.

Sintaxis

-- Crear un savepoint
SAVEPOINT nombre_savepoint;
 
-- Revertir hasta un savepoint
ROLLBACK TO SAVEPOINT nombre_savepoint;
-- o simplemente
ROLLBACK TO nombre_savepoint;
 
-- Eliminar un savepoint sin revertir
RELEASE SAVEPOINT nombre_savepoint;

Ejemplo básico

START TRANSACTION;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Producto A', 100.00, 10, 1);
 
SAVEPOINT sp_a;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Producto B', 200.00, 5, 1);
 
SAVEPOINT sp_b;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Producto C', 300.00, 3, 1);
 
-- Revertir solo Producto C
ROLLBACK TO sp_b;
 
-- Revertir también Producto B
ROLLBACK TO sp_a;
 
COMMIT;
-- Solo Producto A se guardó
SELECT nombre FROM productos WHERE nombre IN ('Producto A', 'Producto B', 'Producto C');
nombre
Producto A
-- Limpieza
DELETE FROM productos WHERE nombre = 'Producto A';

Savepoints anidados

Puedes crear múltiples savepoints y revertir a cualquiera de ellos:

START TRANSACTION;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 1', 10.00, 1, 1);
SAVEPOINT sp1;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 2', 20.00, 1, 1);
SAVEPOINT sp2;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 3', 30.00, 1, 1);
SAVEPOINT sp3;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 4', 40.00, 1, 1);
 
-- Revertir hasta sp2 (elimina Nivel 3 y Nivel 4)
ROLLBACK TO sp2;
 
-- sp3 ya no existe después del rollback
-- sp1 y sp2 siguen disponibles
 
COMMIT;
-- Se guardaron Nivel 1 y Nivel 2
SELECT nombre FROM productos WHERE nombre LIKE 'Nivel%';
nombre
Nivel 1
Nivel 2
DELETE FROM productos WHERE nombre LIKE 'Nivel%';

RELEASE SAVEPOINT

Elimina un savepoint sin revertir los cambios. Es útil para liberar recursos cuando ya no necesitas un punto de restauración.

START TRANSACTION;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Release A', 10.00, 1, 1);
SAVEPOINT sp_release;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Release B', 20.00, 1, 1);
 
-- Ya no necesitamos el savepoint
RELEASE SAVEPOINT sp_release;
 
-- ROLLBACK TO sp_release ya no funciona
-- Pero ROLLBACK completo sí funciona
COMMIT;
DELETE FROM productos WHERE nombre LIKE 'Release%';

Caso práctico: pedido con múltiples productos

DELIMITER //
 
CREATE PROCEDURE sp_pedido_completo(
    IN p_cliente_id INT,
    OUT p_resultado VARCHAR(200)
)
BEGIN
    DECLARE v_pedido_id INT;
    DECLARE v_stock INT;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_resultado = 'Error: pedido cancelado completamente';
    END;
 
    START TRANSACTION;
 
    -- Crear pedido
    INSERT INTO pedidos (cliente_id, fecha, estado, total)
    VALUES (p_cliente_id, NOW(), 'pendiente', 0);
    SET v_pedido_id = LAST_INSERT_ID();
 
    SAVEPOINT sp_detalle;
 
    -- Intentar agregar producto 1
    SELECT stock INTO v_stock FROM productos WHERE id = 1 FOR UPDATE;
    IF v_stock >= 1 THEN
        INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
        VALUES (v_pedido_id, 1, 1, (SELECT precio FROM productos WHERE id = 1));
        UPDATE productos SET stock = stock - 1 WHERE id = 1;
    END IF;
 
    SAVEPOINT sp_detalle2;
 
    -- Intentar agregar producto 2
    SELECT stock INTO v_stock FROM productos WHERE id = 2 FOR UPDATE;
    IF v_stock >= 1 THEN
        INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
        VALUES (v_pedido_id, 2, 1, (SELECT precio FROM productos WHERE id = 2));
        UPDATE productos SET stock = stock - 1 WHERE id = 2;
    ELSE
        -- Si no hay stock del producto 2, revertir solo este detalle
        ROLLBACK TO sp_detalle2;
    END IF;
 
    -- Actualizar total del pedido
    UPDATE pedidos
    SET total = (SELECT COALESCE(SUM(cantidad * precio_unitario), 0)
                 FROM detalle_pedidos WHERE pedido_id = v_pedido_id)
    WHERE id = v_pedido_id;
 
    -- Verificar que el pedido tiene al menos un producto
    IF (SELECT COUNT(*) FROM detalle_pedidos WHERE pedido_id = v_pedido_id) = 0 THEN
        ROLLBACK;
        SET p_resultado = 'Error: ningún producto disponible';
    ELSE
        COMMIT;
        SET p_resultado = CONCAT('Pedido ', v_pedido_id, ' creado exitosamente');
    END IF;
END //
 
DELIMITER ;

Caso práctico: importación con tolerancia a errores

DELIMITER //
 
CREATE PROCEDURE sp_importar_productos()
BEGIN
    DECLARE v_errores INT DEFAULT 0;
    DECLARE v_insertados INT DEFAULT 0;
 
    START TRANSACTION;
 
    -- Producto 1
    SAVEPOINT sp_prod;
    BEGIN
        DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
        BEGIN
            SET v_errores = v_errores + 1;
            ROLLBACK TO sp_prod;
        END;
 
        INSERT INTO productos (nombre, precio, stock, categoria_id)
        VALUES ('Import 1', 100.00, 10, 1);
        SET v_insertados = v_insertados + 1;
    END;
 
    -- Producto 2 (con error intencional: categoría inválida)
    SAVEPOINT sp_prod;
    BEGIN
        DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
        BEGIN
            SET v_errores = v_errores + 1;
            ROLLBACK TO sp_prod;
        END;
 
        INSERT INTO productos (nombre, precio, stock, categoria_id)
        VALUES ('Import 2', 200.00, 5, 9999);
        SET v_insertados = v_insertados + 1;
    END;
 
    -- Producto 3
    SAVEPOINT sp_prod;
    BEGIN
        DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
        BEGIN
            SET v_errores = v_errores + 1;
            ROLLBACK TO sp_prod;
        END;
 
        INSERT INTO productos (nombre, precio, stock, categoria_id)
        VALUES ('Import 3', 300.00, 8, 1);
        SET v_insertados = v_insertados + 1;
    END;
 
    COMMIT;
 
    SELECT v_insertados AS insertados, v_errores AS errores;
END //
 
DELIMITER ;

Comportamiento importante

AcciónEfecto sobre savepoints
ROLLBACK TO spRevierte cambios hasta sp, elimina savepoints posteriores a sp
RELEASE SAVEPOINT spElimina sp sin revertir cambios
COMMITConfirma todo y elimina todos los savepoints
ROLLBACKRevierte todo y elimina todos los savepoints

Savepoints con el mismo nombre

Si creas un savepoint con un nombre que ya existe, el anterior se reemplaza:

START TRANSACTION;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('SP Test 1', 10.00, 1, 1);
SAVEPOINT sp_reuse;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('SP Test 2', 20.00, 1, 1);
SAVEPOINT sp_reuse;  -- Reemplaza el anterior
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('SP Test 3', 30.00, 1, 1);
 
-- Revierte solo SP Test 3 (hasta el segundo sp_reuse)
ROLLBACK TO sp_reuse;
 
COMMIT;
-- Se guardaron SP Test 1 y SP Test 2
DELETE FROM productos WHERE nombre LIKE 'SP Test%';

Limpieza

DROP PROCEDURE IF EXISTS sp_pedido_completo;
DROP PROCEDURE IF EXISTS sp_importar_productos;
DELETE FROM productos WHERE nombre LIKE 'Import%';

En el siguiente artículo veremos los niveles de aislamiento de las transacciones.

Escrito por Eduardo Lázaro