Introducción a las transacciones

Una transacción es un conjunto de operaciones SQL que se ejecutan como una unidad indivisible. O se ejecutan todas correctamente, o no se ejecuta ninguna.

¿Por qué necesitamos transacciones?

Imagina un proceso de compra en nuestra tienda:

-- Paso 1: Crear el pedido
INSERT INTO pedidos (cliente_id, fecha, estado, total)
VALUES (1, NOW(), 'pendiente', 1299.99);
 
-- Paso 2: Agregar el detalle
INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (LAST_INSERT_ID(), 1, 1, 1299.99);
 
-- Paso 3: Descontar el stock
UPDATE productos SET stock = stock - 1 WHERE id = 1;

Si el servidor falla después del paso 1 pero antes del paso 3, tendríamos un pedido sin detalle y sin descuento de stock. Las transacciones evitan este problema.

Propiedades ACID

Las transacciones garantizan cuatro propiedades fundamentales:

PropiedadSignificadoEjemplo
AtomicidadTodo o nadaSi falla el paso 3, se revierten los pasos 1 y 2
ConsistenciaLa base de datos pasa de un estado válido a otroEl stock nunca queda negativo
AislamientoLas transacciones concurrentes no interfieren entre síDos compras simultáneas no venden el mismo producto
DurabilidadLos cambios confirmados persisten ante fallosSi el servidor se reinicia, los datos están intactos

Transacciones en MySQL

Motor de almacenamiento

Solo InnoDB soporta transacciones. MyISAM no las soporta.

-- Verificar el motor de una tabla
SHOW TABLE STATUS WHERE Name = 'productos'\G
-- Verificar el motor predeterminado
SHOW VARIABLES LIKE 'default_storage_engine';
Variable_nameValue
default_storage_engineInnoDB

Autocommit

Por defecto, MySQL opera en modo autocommit: cada sentencia individual es una transacción que se confirma automáticamente.

-- Ver el estado de autocommit
SELECT @@autocommit;
@@autocommit
1
-- Desactivar autocommit (cada sentencia requiere COMMIT explícito)
SET autocommit = 0;
 
-- Reactivar
SET autocommit = 1;

Sintaxis básica

-- Iniciar una transacción explícita
START TRANSACTION;
 
-- Ejecutar operaciones
INSERT INTO pedidos (cliente_id, fecha, estado, total)
VALUES (1, NOW(), 'pendiente', 500.00);
 
UPDATE productos SET stock = stock - 1 WHERE id = 1;
 
-- Confirmar todos los cambios
COMMIT;
-- Si algo falla, revertir todo
START TRANSACTION;
 
UPDATE productos SET stock = stock - 1 WHERE id = 1;
 
-- Revertir los cambios
ROLLBACK;
-- El stock vuelve a su valor original

Ejemplo completo: proceso de compra

DELIMITER //
 
CREATE PROCEDURE sp_crear_pedido(
    IN p_cliente_id INT,
    IN p_producto_id INT,
    IN p_cantidad INT,
    OUT p_pedido_id INT
)
BEGIN
    DECLARE v_precio DECIMAL(10,2);
    DECLARE v_stock INT;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_pedido_id = -1;
    END;
 
    START TRANSACTION;
 
    -- Verificar stock
    SELECT precio, stock INTO v_precio, v_stock
    FROM productos
    WHERE id = p_producto_id
    FOR UPDATE;
 
    IF v_stock < p_cantidad THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Stock insuficiente';
    END IF;
 
    -- Crear pedido
    INSERT INTO pedidos (cliente_id, fecha, estado, total)
    VALUES (p_cliente_id, NOW(), 'pendiente', v_precio * p_cantidad);
 
    SET p_pedido_id = LAST_INSERT_ID();
 
    -- Agregar detalle
    INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
    VALUES (p_pedido_id, p_producto_id, p_cantidad, v_precio);
 
    -- Descontar stock
    UPDATE productos
    SET stock = stock - p_cantidad
    WHERE id = p_producto_id;
 
    COMMIT;
END //
 
DELIMITER ;
-- Usar el procedimiento
CALL sp_crear_pedido(1, 3, 2, @pedido_id);
SELECT @pedido_id;

Sentencias que causan COMMIT implícito

Algunas sentencias confirman automáticamente cualquier transacción abierta:

CategoríaSentencias
DDLCREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE TABLE
AdministraciónGRANT, REVOKE, SET PASSWORD
ControlLOCK TABLES, UNLOCK TABLES
Carga de datosLOAD DATA
START TRANSACTION;
 
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Test', 10.00, 5, 1);
 
-- Esto causa COMMIT implícito del INSERT anterior
CREATE TABLE temp_test (id INT);
 
-- Ya no se puede hacer ROLLBACK del INSERT
ROLLBACK; -- No revierte nada
-- Limpieza
DROP TABLE IF EXISTS temp_test;
DELETE FROM productos WHERE nombre = 'Test';

Transacciones de solo lectura

Para consultas que no modifican datos, puedes declarar la transacción como solo lectura, lo cual permite optimizaciones:

START TRANSACTION READ ONLY;
 
SELECT * FROM productos WHERE precio > 500;
SELECT COUNT(*) FROM pedidos WHERE estado = 'completado';
 
COMMIT;

Cuándo usar transacciones

Escenario¿Transacción?Razón
Proceso de compraMúltiples tablas deben actualizarse juntas
Transferencia entre cuentasDebitar y acreditar son inseparables
Consulta simpleNoAutocommit es suficiente
INSERT individualNoAutocommit es suficiente
Importación masivaRendimiento y consistencia
Actualización en lotePoder revertir si algo falla

Limpieza

DROP PROCEDURE IF EXISTS sp_crear_pedido;

En el siguiente artículo veremos en detalle COMMIT y ROLLBACK.

Escrito por Eduardo Lázaro