ON DELETE CASCADE

ON DELETE CASCADE es una acción referencial que se define junto con una clave foránea. Cuando eliminas una fila de la tabla padre, MySQL elimina automáticamente todas las filas de la tabla hija que la referencian. Es la forma más práctica de mantener la integridad referencial en relaciones donde las filas hijas no tienen sentido sin su fila padre.

Sintaxis

FOREIGN KEY (columna) REFERENCES tabla_padre(columna_padre)
    ON DELETE CASCADE

Ejemplo práctico

Consideremos un blog con artículos y comentarios. Si eliminamos un artículo, sus comentarios deberían eliminarse automáticamente:

CREATE TABLE articulos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    titulo VARCHAR(200) NOT NULL,
    contenido TEXT
);
 
CREATE TABLE comentarios (
    id INT AUTO_INCREMENT PRIMARY KEY,
    articulo_id INT NOT NULL,
    autor VARCHAR(100) NOT NULL,
    texto TEXT NOT NULL,
    CONSTRAINT fk_comentario_articulo
        FOREIGN KEY (articulo_id) REFERENCES articulos(id)
        ON DELETE CASCADE
);
 
INSERT INTO articulos VALUES
(1, 'MySQL básico', 'Introducción a MySQL...'),
(2, 'JOINs avanzados', 'Los JOINs permiten...');
 
INSERT INTO comentarios (articulo_id, autor, texto) VALUES
(1, 'Ana', 'Muy útil, gracias'),
(1, 'Luis', 'Excelente explicación'),
(1, 'Eva', '¿Puedes hacer uno de PostgreSQL?'),
(2, 'Carlos', 'Los diagramas ayudan mucho');

Verificamos los datos:

SELECT a.titulo, COUNT(c.id) AS comentarios
FROM articulos a
LEFT JOIN comentarios c ON a.id = c.articulo_id
GROUP BY a.id, a.titulo;
titulocomentarios
MySQL básico3
JOINs avanzados1

Ahora eliminamos el artículo 1:

DELETE FROM articulos WHERE id = 1;
Query OK, 1 row affected (0.01 sec)

MySQL elimina automáticamente los 3 comentarios asociados:

SELECT * FROM comentarios;
idarticulo_idautortexto
42CarlosLos diagramas ayudan mucho

Solo queda el comentario del artículo 2, que no fue eliminado.

CASCADE en cadena

Si hay múltiples niveles de relaciones con CASCADE, la eliminación se propaga a través de todos ellos:

CREATE TABLE foros (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL
);
 
CREATE TABLE hilos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    foro_id INT NOT NULL,
    titulo VARCHAR(200) NOT NULL,
    FOREIGN KEY (foro_id) REFERENCES foros(id) ON DELETE CASCADE
);
 
CREATE TABLE respuestas (
    id INT AUTO_INCREMENT PRIMARY KEY,
    hilo_id INT NOT NULL,
    texto TEXT NOT NULL,
    FOREIGN KEY (hilo_id) REFERENCES hilos(id) ON DELETE CASCADE
);
 
INSERT INTO foros VALUES (1, 'MySQL');
INSERT INTO hilos VALUES (1, 1, '¿Cómo hacer un JOIN?');
INSERT INTO respuestas VALUES (1, 1, 'Usa INNER JOIN...'), (2, 1, 'También puedes usar LEFT JOIN...');

Al eliminar el foro, se eliminan sus hilos, y al eliminarse los hilos, se eliminan sus respuestas:

DELETE FROM foros WHERE id = 1;
 
SELECT * FROM hilos;     -- Empty set
SELECT * FROM respuestas; -- Empty set

La eliminación se propaga automáticamente por toda la cadena.

CASCADE en tienda_mysql

Nuestra base de datos usa ON DELETE CASCADE en dos relaciones clave:

pedidos.cliente_id referencia clientes.id con ON DELETE CASCADE. Si eliminamos un cliente, se eliminan todos sus pedidos.

detalle_pedidos.pedido_id referencia pedidos.id con ON DELETE CASCADE. Si se elimina un pedido, se eliminan automáticamente sus líneas de detalle.

Esto crea una cadena: eliminar un cliente elimina sus pedidos, y eliminar esos pedidos elimina sus detalles. Tres tablas se limpian con un solo DELETE.

-- Ver cuántos registros tiene Diego (cliente 20)
SELECT
    (SELECT COUNT(*) FROM pedidos WHERE cliente_id = 20) AS pedidos,
    (SELECT COUNT(*) FROM detalle_pedidos dp
     JOIN pedidos p ON dp.pedido_id = p.id
     WHERE p.cliente_id = 20) AS detalles;
pedidosdetalles
00

Diego no tiene pedidos, así que un DELETE sobre él no afectaría a otras tablas. Pero si María (con 3 pedidos) fuera eliminada, sus pedidos y todos sus detalles desaparecerían automáticamente.

Comparación: CASCADE vs SET NULL vs RESTRICT

Cada acción tiene un caso de uso diferente:

-- CASCADE: las filas hijas no tienen sentido sin el padre
-- Ejemplo: líneas de detalle sin pedido
FOREIGN KEY (pedido_id) REFERENCES pedidos(id) ON DELETE CASCADE
 
-- SET NULL: las filas hijas pueden existir sin padre
-- Ejemplo: empleado sin supervisor
FOREIGN KEY (supervisor_id) REFERENCES empleados(id) ON DELETE SET NULL
 
-- RESTRICT: prohibir la eliminación si hay dependencias
-- Ejemplo: no eliminar un producto que aparece en pedidos
FOREIGN KEY (producto_id) REFERENCES productos(id) ON DELETE RESTRICT

CASCADE es apropiado para relaciones de composición, donde la parte no puede existir sin el todo: comentarios de un artículo, líneas de un pedido, archivos de un proyecto.

SET NULL es apropiado para relaciones de asociación, donde la fila hija tiene existencia independiente: un empleado cuyo supervisor es eliminado debería seguir existiendo, solo pierde la referencia.

RESTRICT es apropiado para proteger la integridad histórica: un producto que aparece en pedidos anteriores no debería poder eliminarse, porque destruiría el historial de ventas.

ON UPDATE CASCADE

Además de ON DELETE, puedes definir ON UPDATE CASCADE para propagar cambios en la clave primaria:

CREATE TABLE codigos_producto (
    sku CHAR(8) PRIMARY KEY,
    nombre VARCHAR(100)
);
 
CREATE TABLE stock_tienda (
    id INT AUTO_INCREMENT PRIMARY KEY,
    sku CHAR(8),
    cantidad INT,
    FOREIGN KEY (sku) REFERENCES codigos_producto(sku)
        ON DELETE CASCADE
        ON UPDATE CASCADE
);
 
INSERT INTO codigos_producto VALUES ('PROD-001', 'Laptop');
INSERT INTO stock_tienda (sku, cantidad) VALUES ('PROD-001', 50);
 
-- Cambiar el SKU propaga el cambio
UPDATE codigos_producto SET sku = 'LAP-0001' WHERE sku = 'PROD-001';
 
SELECT * FROM stock_tienda;
idskucantidad
1LAP-000150

En la práctica, ON UPDATE CASCADE se usa poco porque las claves primarias basadas en AUTO_INCREMENT no cambian.

Precauciones

ON DELETE CASCADE puede eliminar más datos de los esperados. Si no eres consciente de las relaciones en cadena, un DELETE sobre una tabla padre puede borrar miles de registros en tablas hijas y nietas.

Antes de usar CASCADE, evalúa si realmente quieres que los datos hijos se eliminen automáticamente. Para datos con valor histórico (como pedidos completados), RESTRICT suele ser más apropiado.

Para ver qué tablas tienen CASCADE configurado:

SELECT
    TABLE_NAME,
    COLUMN_NAME,
    REFERENCED_TABLE_NAME,
    REFERENCED_COLUMN_NAME,
    DELETE_RULE
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
JOIN information_schema.KEY_COLUMN_USAGE kcu
    USING (CONSTRAINT_SCHEMA, CONSTRAINT_NAME)
WHERE rc.CONSTRAINT_SCHEMA = DATABASE()
  AND DELETE_RULE = 'CASCADE';

Limpieza

DROP TABLE IF EXISTS respuestas;
DROP TABLE IF EXISTS hilos;
DROP TABLE IF EXISTS foros;
DROP TABLE IF EXISTS comentarios;
DROP TABLE IF EXISTS articulos;
DROP TABLE IF EXISTS stock_tienda;
DROP TABLE IF EXISTS codigos_producto;

En el siguiente artículo veremos la restricción UNIQUE, que garantiza que los valores de una columna no se repitan.

Escrito por Eduardo Lázaro