Un trigger es un bloque de código SQL que se ejecuta automáticamente al ocurrir un evento INSERT, UPDATE o DELETE sobre una tabla. Son muy útiles para mantener la integridad de los datos, auditar cambios y automatizar reglas de negocio.
Un trigger se asocia a una tabla y se define para ejecutarse antes (BEFORE) o después (AFTER) de un evento INSERT, UPDATE o DELETE.
CREATE TRIGGER nombre_trigger
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON nombre_tabla
FOR EACH ROW
BEGIN
-- instrucciones SQL
END;
FOR EACH ROW es obligatorio. Significa que el trigger se ejecutará una vez por cada fila afectada por la sentencia DML. No existe en MySQL el nivel FOR EACH STATEMENT (por sentencia).
Desde los inicios, los triggers se han usado para tres propósitos fundamentales. A continuación se detallan con ejemplos conceptuales:
Estos usos se pueden combinar, pero es importante no mezclar lógica que pueda causar efectos secundarios no deseados. La sección "Automatización vs Auditoría" más abajo muestra dos triggers distintos sobre la misma tabla para que veas la diferencia.
Tabla productos y trigger que actualiza el precio automáticamente cuando el costo cambia.
CREATE TABLE productos (
id INT PRIMARY KEY,
nombre VARCHAR(100),
costo DECIMAL(10,2),
precio DECIMAL(10,2),
proveedor_id INT
);
DELIMITER $$
CREATE TRIGGER actualizarPrecioProducto
BEFORE UPDATE ON productos
FOR EACH ROW
BEGIN
IF NEW.costo <> OLD.costo THEN
SET NEW.precio = NEW.costo * 2;
END IF;
END$$
DELIMITER ;
Importante: Cuando ejecutamos UPDATE productos SET costo = costo * 1.10 WHERE proveedor_id = 1;, el motor:
costo para cada fila afectada (ej: 50 → 55).BEFORE UPDATE para esa fila. Dentro del trigger, OLD contiene los valores actuales (aún intactos en la tabla) y NEW contiene los valores calculados (aún no escritos).NEW.precio.NEW.Partimos de los siguientes registros iniciales:
| id | nombre | costo | precio | proveedor_id |
|---|---|---|---|---|
| 1 | Teclado RGB | 50.00 | 100.00 | 1 |
| 2 | Mouse inalámbrico | 30.00 | 60.00 | 1 |
| 3 | Monitor 24" | 200.00 | 400.00 | 2 |
Ejecutamos: UPDATE productos SET costo = costo * 1.10 WHERE proveedor_id = 1; (afecta id=1 y id=2).
| Momentos | costo | precio | Acción |
|---|---|---|---|
| OLD (valor actual en tabla) | 50.00 | 100.00 | |
| NEW calculado por la sentencia (antes del trigger) | 55.00 (50*1.10) | 100.00 | |
| Dentro del trigger: IF 55.00 <> 50.00 -> verdadero | SET NEW.precio = 55.00 * 2 = 110.00 | ||
| NEW final (después del trigger, se escribe) | 55.00 | 110.00 | ✓ |
| Momentos | costo | precio | Acción |
|---|---|---|---|
| OLD (valor actual en tabla) | 30.00 | 60.00 | |
| NEW calculado por la sentencia (antes del trigger) | 33.00 (30*1.10) | 60.00 | |
| Dentro del trigger: IF 33.00 <> 30.00 -> verdadero | SET NEW.precio = 33.00 * 2 = 66.00 | ||
| NEW final (después del trigger, se escribe) | 33.00 | 66.00 | ✓ |
Tabla productos después del UPDATE (estado final):
| id | nombre | costo | precio | proveedor_id |
|---|---|---|---|---|
| 1 | Teclado RGB | 55.00 | 110.00 | 1 |
| 2 | Mouse inalámbrico | 33.00 | 66.00 | 1 |
| 3 | Monitor 24" | 200.00 | 400.00 | 2 |
En INSERT, OLD no existe. En DELETE, NEW no existe.
Existen situaciones donde el trigger no requiere acceder a los valores específicos de la fila, sino solo a la operación en sí.
Ejemplo 1: Trigger AFTER INSERT sin usar NEW (solo auditoría de operación)
CREATE TRIGGER after_insert_log
AFTER INSERT ON tablita
FOR EACH ROW
BEGIN
INSERT INTO log_auditoria (accion_ejecutada, nombre_tabla, fecha_hora)
VALUES ('INSERT', 'tablita', NOW());
END;
No necesitamos NEW porque solo registramos que ocurrió una inserción, no los datos insertados.
Ejemplo 2: Trigger BEFORE DELETE sin usar OLD (limpieza genérica)
CREATE TRIGGER before_delete_limpieza
BEFORE DELETE ON pedidos
FOR EACH ROW
BEGIN
DELETE FROM detalle_pedido WHERE id_pedido = OLD.id_pedido;
END;
Aquí sí se usa OLD, pero el PDF menciona que podría no necesitarse si la limpieza no depende del valor, por ejemplo eliminando registros más antiguos que una fecha fija.
Ejemplo 3: Trigger AFTER DELETE que borra registros en otra tabla
CREATE TRIGGER after_delete_cleanup
AFTER DELETE ON employees
FOR EACH ROW
BEGIN
DELETE FROM employee_history WHERE employee_id = OLD.employee_id;
END;
Igual que antes, se usa OLD para identificar la relación.
¿Son tablas o CTEs? No. NEW y OLD son referencias temporales a los datos de una fila específica durante la ejecución del trigger. No existen físicamente, no ocupan espacio, y no se pueden consultar fuera del trigger.
| Categoría | Capacidad / Restricción | ¿Permitido? | Notas / Versión |
|---|---|---|---|
| Objetos objetivo | Tablas regulares (InnoDB, MyISAM) | SÍ | Puede leer/modificar cualquier tabla, incluso de otras bases de datos (cross-database: `otra_bd.tabla`). |
| Tablas del sistema (`mysql`, `information_schema`) | NO | No se puede asociar un trigger a una tabla del sistema. | |
| Tablas temporales | NO | No se puede crear un trigger sobre una tabla temporal. | |
| DML en otras tablas | INSERT/UPDATE/DELETE en otras tablas | SÍ | Válido para auditoría o cascada. |
| Modificar la misma tabla que disparó el trigger (excepto vía NEW) | NO | `UPDATE productos SET ...` (sin usar NEW) da error: la tabla ya está en uso. | |
| Transacciones | COMMIT / ROLLBACK explícitos | NO | Prohibido iniciar o finalizar transacciones dentro del trigger. |
| Ser alcanzado por ROLLBACK externo | SÍ | Si la sentencia DML falla (o se usa SIGNAL), los cambios del trigger se revierten. | |
| Manejo de errores | Definir HANDLERs (CONTINUE/EXIT) | SÍ | Siempre disponible (desde MySQL 5.0). |
| Usar SIGNAL / RESIGNAL | SÍ | Desde MySQL 5.5. | |
| Control de flujo | IF, CASE, LOOP, WHILE, REPEAT, LEAVE | SÍ | Todas las estructuras de stored procedures. |
| Cursores | Cursores explícitos (DECLARE CURSOR, FETCH) | SÍ | Se puede recorrer un conjunto de resultados. |
| FOR EACH STATEMENT | NO | MySQL solo soporta nivel fila. | |
| Lenguaje | Variables locales (DECLARE) | SÍ | Ámbito dentro del bloque. |
| Variables de usuario (@var) | SÍ | Pueden comunicar valores después de la ejecución. | |
| Prepared Statements (SQL dinámico) | NO | PREPARE, EXECUTE no permitidos. | |
| Devolver resultados al cliente (SELECT) | NO | No puede retornar conjuntos de filas. Alternativa: usar variables de sesión o tablas de log. | |
| Modularidad | Llamar a Stored Procedures (CALL) | SÍ | El SP no debe devolver resultados al cliente ni usar instrucciones prohibidas. |
| DDL | CREATE, ALTER, DROP | NO | No se permite modificar la estructura de objetos. |
SET @resultado = '...';) que luego el cliente lea, o escriba en una tabla de logs. No intente usar SELECT para mostrar resultados porque generará un error.
Ejemplo con tabla pedidos y campo estado:
CREATE TABLE pedidos (
id INT PRIMARY KEY,
cliente VARCHAR(100),
estado VARCHAR(20),
fecha_entrega DATE
);
CREATE TABLE auditoria_pedidos (
id INT AUTO_INCREMENT PRIMARY KEY,
pedido_id INT,
estado_anterior VARCHAR(20),
estado_nuevo VARCHAR(20),
usuario VARCHAR(50),
fecha_cambio TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Automatización (activo): BEFORE UPDATE que asigna fecha_entrega automáticamente.
CREATE TRIGGER auto_fecha_entrega
BEFORE UPDATE ON pedidos FOR EACH ROW
BEGIN
IF NEW.estado = 'enviado' AND OLD.estado != 'enviado' THEN
SET NEW.fecha_entrega = CURDATE();
END IF;
END;
Auditoría (pasivo): AFTER UPDATE que registra el cambio en otra tabla.
CREATE TRIGGER audit_estado_pedido
AFTER UPDATE ON pedidos FOR EACH ROW
BEGIN
INSERT INTO auditoria_pedidos (pedido_id, estado_anterior, estado_nuevo, usuario)
VALUES (NEW.id, OLD.estado, NEW.estado, USER());
END;
max_sp_recursion_depth.FLUSH TABLES o reinicio.-- Ver todos los triggers de la BD actual
SHOW TRIGGERS;
-- Ver triggers de una BD específica
SHOW TRIGGERS FROM nombre_base_datos;
-- Ver triggers de una tabla específica
SHOW TRIGGERS FROM tienda WHERE `Table` = 'productos';
-- Consultar usando INFORMATION_SCHEMA (más potente)
SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT
FROM INFORMATION_SCHEMA.TRIGGERS
WHERE TRIGGER_SCHEMA = 'nombre_base_datos';
-- Eliminar trigger
DROP TRIGGER IF EXISTS nombre_trigger;
El comando SHOW TRIGGERS devuelve columnas como: Trigger, Event, Table, Statement, Timing, Created, definer, etc.
¿Puedo tener dos triggers BEFORE UPDATE en la misma tabla? Técnicamente sí desde 5.7.2, pero es mala práctica. Use uno solo.
¿Los triggers funcionan con INSERT ... ON DUPLICATE KEY UPDATE? Sí: si es INSERT, dispara triggers de INSERT; si es UPDATE, dispara triggers de UPDATE.
¿Se puede leer el valor autoincremental en BEFORE INSERT? No, NEW.id vale 0 o NULL. Usar AFTER INSERT.
¿Cómo hago para que un trigger revierta la operación sin error? No se puede directamente. Use SIGNAL para abortar con error.
¿Un trigger puede actualizar otra tabla y esa tabla a su vez tener un trigger? Sí, puede haber cascada de triggers.
Documento completo basado en MySQL 8.0. Incluye todos los conceptos del material original: definición, tipos BEFORE/AFTER, sintaxis, ejemplo, NEW/OLD, usos (auditoría, validación, automatización), ejemplos visuales paso a paso (con tablas de evolución), casos sin NEW/OLD, disponibilidad por operación, tabla de capacidades, limitaciones, administración y preguntas frecuentes. Corregido el error del precio del mouse (66.00).