Triggers en SQL: qué son y cómo automatizar acciones en la base de datos

D
DanisCh
• 13 min de lectura
Triggers en SQL: qué son y cómo automatizar acciones en la base de datos
SQL Conceptos de Base de Datos

Un trigger (o disparador) es un bloque de código que el motor de base de datos ejecuta automáticamente cuando ocurre un evento concreto en una tabla: una inserción, una actualización o un borrado. No se llaman explícitamente desde la aplicación, sino que se activan solos, como una alarma que se dispara cuando se cumplen las condiciones.

Son una herramienta poderosa para garantizar la integridad de los datos, mantener auditorías automáticas o aplicar reglas de negocio directamente en la base de datos, independientemente de qué aplicación o cliente esté modificando los datos. En esta guía vas a ver cómo funcionan, cómo crearlos en MySQL y PostgreSQL, y cuándo tiene sentido usarlos.

El esquema de ejemplo

-- Tabla principal de productos
CREATE TABLE productos (
  id          INT PRIMARY KEY AUTO_INCREMENT,
  nombre      VARCHAR(100) NOT NULL,
  precio      DECIMAL(10, 2) NOT NULL,
  stock       INT DEFAULT 0,
  actualizado TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de pedidos
CREATE TABLE pedidos (
  id          INT PRIMARY KEY AUTO_INCREMENT,
  producto_id INT REFERENCES productos(id),
  cantidad    INT NOT NULL,
  total       DECIMAL(10, 2),
  fecha       TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  estado      VARCHAR(20) DEFAULT 'pendiente'
);

-- Tabla de auditoría: registra cada cambio de precio
CREATE TABLE historial_precios (
  id          INT PRIMARY KEY AUTO_INCREMENT,
  producto_id INT NOT NULL,
  precio_anterior DECIMAL(10, 2),
  precio_nuevo    DECIMAL(10, 2),
  modificado_por  VARCHAR(100),
  modificado_en   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de auditoría general de cambios
CREATE TABLE log_cambios (
  id         INT PRIMARY KEY AUTO_INCREMENT,
  tabla      VARCHAR(50),
  accion     VARCHAR(10),
  registro_id INT,
  datos      TEXT,
  usuario    VARCHAR(100),
  fecha      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Anatomía de un trigger

Todo trigger se define con cuatro elementos:

  • El evento: qué operación lo activa — INSERT, UPDATE o DELETE.
  • El momento: BEFORE (antes de que se ejecute la operación) o AFTER (después).
  • La tabla: sobre qué tabla se vigila el evento.
  • El cuerpo: el código SQL que se ejecuta cuando el trigger se activa.

Dentro del cuerpo del trigger tienes acceso a dos registros especiales:

  • NEW: los valores que va a tener la fila después de la operación (disponible en INSERT y UPDATE).
  • OLD: los valores que tenía la fila antes de la operación (disponible en UPDATE y DELETE).
-- Tabla de referencia: cuándo están disponibles NEW y OLD
-- INSERT: solo NEW (no hay valor anterior)
-- UPDATE: OLD (antes) y NEW (después)
-- DELETE: solo OLD (no hay valor posterior)

Triggers en MySQL

Trigger AFTER INSERT: actualizar stock al crear un pedido

DELIMITER //

CREATE TRIGGER actualizar_stock_tras_pedido
AFTER INSERT ON pedidos
FOR EACH ROW
BEGIN
    UPDATE productos
    SET stock = stock - NEW.cantidad
    WHERE id = NEW.producto_id;
END //

DELIMITER ;

-- Prueba: insertar un pedido y verificar que el stock baja
INSERT INTO pedidos (producto_id, cantidad, total) VALUES (1, 3, 59.97);
SELECT stock FROM productos WHERE id = 1;   -- debería ser 3 menos que antes

Trigger BEFORE INSERT: calcular el total automáticamente

Un trigger BEFORE puede modificar los valores que van a insertarse o actualizarse antes de que el motor los confirme. Esto es útil para calcular campos derivados o validar datos.

DELIMITER //

CREATE TRIGGER calcular_total_pedido
BEFORE INSERT ON pedidos
FOR EACH ROW
BEGIN
    DECLARE precio_unitario DECIMAL(10, 2);

    SELECT precio INTO precio_unitario
    FROM productos
    WHERE id = NEW.producto_id;

    -- Modificar NEW.total antes de que se inserte el registro
    SET NEW.total = precio_unitario * NEW.cantidad;
END //

DELIMITER ;

-- Ahora el total se calcula automáticamente sin importar lo que el cliente envíe
INSERT INTO pedidos (producto_id, cantidad) VALUES (1, 5);
SELECT total FROM pedidos ORDER BY id DESC LIMIT 1;   -- calculado automáticamente

Trigger BEFORE UPDATE: validar datos antes de actualizarlos

DELIMITER //

CREATE TRIGGER validar_precio_positivo
BEFORE UPDATE ON productos
FOR EACH ROW
BEGIN
    -- Impedir que el precio se actualice a un valor negativo o cero
    IF NEW.precio <= 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'El precio debe ser mayor que cero';
    END IF;

    -- Impedir reducir el precio más de un 50% de golpe
    IF NEW.precio < OLD.precio * 0.5 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'No se puede reducir el precio más de un 50% en una sola operación';
    END IF;
END //

DELIMITER ;

-- Esto ahora falla:
-- UPDATE productos SET precio = -10 WHERE id = 1;
-- UPDATE productos SET precio = 1 WHERE id = 1;   (si el precio era 100)

Trigger AFTER UPDATE: registrar auditoría de cambios de precio

DELIMITER //

CREATE TRIGGER auditar_cambio_precio
AFTER UPDATE ON productos
FOR EACH ROW
BEGIN
    -- Solo registrar si el precio realmente cambió
    IF OLD.precio <> NEW.precio THEN
        INSERT INTO historial_precios (
            producto_id,
            precio_anterior,
            precio_nuevo,
            modificado_por
        ) VALUES (
            NEW.id,
            OLD.precio,
            NEW.precio,
            USER()   -- función de MySQL que devuelve el usuario de la sesión actual
        );
    END IF;
END //

DELIMITER ;

-- Probar:
UPDATE productos SET precio = 39.99 WHERE id = 1;
SELECT * FROM historial_precios;

Trigger AFTER DELETE: auditoría de borrados

DELIMITER //

CREATE TRIGGER auditar_borrado_producto
AFTER DELETE ON productos
FOR EACH ROW
BEGIN
    INSERT INTO log_cambios (tabla, accion, registro_id, datos, usuario)
    VALUES (
        'productos',
        'DELETE',
        OLD.id,
        CONCAT('nombre=', OLD.nombre, ', precio=', OLD.precio, ', stock=', OLD.stock),
        USER()
    );
END //

DELIMITER ;

-- Ahora cada DELETE queda registrado automáticamente
DELETE FROM productos WHERE id = 5;
SELECT * FROM log_cambios WHERE tabla = 'productos';

Ver y eliminar triggers en MySQL

-- Listar todos los triggers de la base de datos actual
SHOW TRIGGERS;

-- Listar triggers de una tabla específica
SHOW TRIGGERS LIKE 'productos';

-- Ver la definición de un trigger
SHOW CREATE TRIGGER actualizar_stock_tras_pedido;

-- Eliminar un trigger
DROP TRIGGER IF EXISTS actualizar_stock_tras_pedido;

Triggers en PostgreSQL

En PostgreSQL, los triggers requieren dos pasos: primero se define una función trigger (una función especial que devuelve TRIGGER), y luego se crea el trigger que asocia esa función con la tabla y el evento.

Trigger AFTER INSERT: actualizar stock al crear un pedido

-- Paso 1: crear la función trigger
CREATE OR REPLACE FUNCTION fn_actualizar_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    UPDATE productos
    SET stock = stock - NEW.cantidad
    WHERE id = NEW.producto_id;

    RETURN NEW;   -- en triggers AFTER, el valor devuelto se ignora; en BEFORE, determina si continúa
END;
$$;

-- Paso 2: crear el trigger y asociarlo a la función
CREATE TRIGGER trg_actualizar_stock
AFTER INSERT ON pedidos
FOR EACH ROW
EXECUTE FUNCTION fn_actualizar_stock();

-- Probar
INSERT INTO pedidos (producto_id, cantidad, total) VALUES (1, 2, 39.98);
SELECT stock FROM productos WHERE id = 1;

Trigger BEFORE INSERT: calcular total y validar stock

CREATE OR REPLACE FUNCTION fn_preparar_pedido()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
    v_precio DECIMAL(10, 2);
    v_stock  INT;
BEGIN
    SELECT precio, stock INTO v_precio, v_stock
    FROM productos
    WHERE id = NEW.producto_id;

    -- Validar stock disponible
    IF v_stock < NEW.cantidad THEN
        RAISE EXCEPTION 'Stock insuficiente: disponible %, solicitado %',
            v_stock, NEW.cantidad;
    END IF;

    -- Calcular el total automáticamente
    NEW.total := v_precio * NEW.cantidad;

    RETURN NEW;   -- en triggers BEFORE, devolver NEW confirma la operación
                  -- devolver NULL la cancela
END;
$$;

CREATE TRIGGER trg_preparar_pedido
BEFORE INSERT ON pedidos
FOR EACH ROW
EXECUTE FUNCTION fn_preparar_pedido();

Trigger AFTER UPDATE: auditoría de cambios de precio

CREATE OR REPLACE FUNCTION fn_auditar_precio()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    IF OLD.precio IS DISTINCT FROM NEW.precio THEN
        INSERT INTO historial_precios (
            producto_id,
            precio_anterior,
            precio_nuevo,
            modificado_por
        ) VALUES (
            NEW.id,
            OLD.precio,
            NEW.precio,
            CURRENT_USER   -- usuario de la sesión actual en PostgreSQL
        );
    END IF;

    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_auditar_precio
AFTER UPDATE OF precio ON productos   -- solo se activa si 'precio' cambió
FOR EACH ROW
EXECUTE FUNCTION fn_auditar_precio();

-- "AFTER UPDATE OF precio" es más eficiente que "AFTER UPDATE":
-- solo se activa cuando la columna 'precio' está en el SET
UPDATE productos SET precio = 49.99 WHERE id = 1;    -- activa el trigger
UPDATE productos SET stock = 100   WHERE id = 1;     -- NO activa el trigger

Trigger BEFORE UPDATE: actualizar timestamp automáticamente

CREATE OR REPLACE FUNCTION fn_actualizar_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    NEW.actualizado := NOW();
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_actualizar_timestamp
BEFORE UPDATE ON productos
FOR EACH ROW
EXECUTE FUNCTION fn_actualizar_timestamp();

-- Ahora cada UPDATE en productos actualiza automáticamente la columna 'actualizado'
UPDATE productos SET stock = 50 WHERE id = 1;
SELECT actualizado FROM productos WHERE id = 1;   -- refleja el momento del UPDATE

Trigger con condición WHEN

PostgreSQL permite añadir una condición WHEN al trigger para que solo se active si se cumple una condición específica, sin necesidad de poner el IF dentro de la función.

CREATE OR REPLACE FUNCTION fn_notificar_stock_bajo()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    -- En producción esto podría publicar en un canal con pg_notify
    RAISE NOTICE 'Alerta: el stock del producto % ha bajado a %', NEW.id, NEW.stock;
    RETURN NEW;
END;
$$;

-- El trigger solo se activa cuando el stock cae por debajo de 10
CREATE TRIGGER trg_stock_bajo
AFTER UPDATE OF stock ON productos
FOR EACH ROW
WHEN (NEW.stock < 10 AND OLD.stock >= 10)   -- condición evaluada antes de llamar a la función
EXECUTE FUNCTION fn_notificar_stock_bajo();

-- La función solo se llama cuando el stock cruza el umbral de 10
UPDATE productos SET stock = 5 WHERE id = 1;    -- activa el trigger (pasó de ≥10 a <10)
UPDATE productos SET stock = 3 WHERE id = 1;    -- NO activa (ya estaba <10)
UPDATE productos SET stock = 15 WHERE id = 1;   -- NO activa (subió por encima)
UPDATE productos SET stock = 8 WHERE id = 1;    -- activa de nuevo (vuelve a cruzar)

Ver y eliminar triggers en PostgreSQL

-- Listar todos los triggers de una tabla
SELECT trigger_name, event_manipulation, action_timing, action_statement
FROM information_schema.triggers
WHERE event_object_table = 'productos';

-- Listar triggers con más detalle desde el catálogo del sistema
SELECT
  tgname  AS nombre_trigger,
  relname AS tabla,
  proname AS funcion
FROM pg_trigger t
JOIN pg_class c  ON c.oid = t.tgrelid
JOIN pg_proc p   ON p.oid = t.tgfoid
WHERE NOT tgisinternal
ORDER BY relname, tgname;

-- Eliminar un trigger
DROP TRIGGER IF EXISTS trg_actualizar_stock ON pedidos;

-- La función permanece, solo se elimina la asociación
-- Para eliminar también la función:
DROP FUNCTION IF EXISTS fn_actualizar_stock();

Tabla comparativa MySQL vs PostgreSQL

ConceptoMySQLPostgreSQL
DefiniciónTodo en CREATE TRIGGERFunción separada + CREATE TRIGGER
Lanzar errorSIGNAL SQLSTATE '45000'RAISE EXCEPTION
Usuario actualUSER()CURRENT_USER
Trigger solo en columnasNo (en el evento de tabla completa)Sí: UPDATE OF columna
Condición en el triggerIF dentro del cuerpoCláusula WHEN en el trigger
Valor de retornoNo aplicaRETURN NEW (continúa) / RETURN NULL (cancela en BEFORE)
Reutilizar lógicaDifícil: la lógica está en el triggerFácil: la función puede usarse en varios triggers
Comparar nulosOLD.precio <> NEW.precioOLD.precio IS DISTINCT FROM NEW.precio (más seguro con NULLs)

Casos de uso habituales

Auditoría automática genérica

-- PostgreSQL: función genérica de auditoría reutilizable en cualquier tabla
CREATE OR REPLACE FUNCTION fn_auditoria_generica()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO log_cambios (tabla, accion, registro_id, datos, usuario)
    VALUES (
        TG_TABLE_NAME,   -- variable especial: nombre de la tabla que activó el trigger
        TG_OP,           -- variable especial: 'INSERT', 'UPDATE' o 'DELETE'
        CASE TG_OP
            WHEN 'DELETE' THEN OLD.id
            ELSE NEW.id
        END,
        CASE TG_OP
            WHEN 'INSERT' THEN row_to_json(NEW)::TEXT
            WHEN 'UPDATE' THEN json_build_object('antes', row_to_json(OLD), 'despues', row_to_json(NEW))::TEXT
            WHEN 'DELETE' THEN row_to_json(OLD)::TEXT
        END,
        CURRENT_USER
    );
    RETURN NEW;
END;
$$;

-- Reutilizar la misma función en varias tablas
CREATE TRIGGER trg_auditoria_productos
AFTER INSERT OR UPDATE OR DELETE ON productos
FOR EACH ROW EXECUTE FUNCTION fn_auditoria_generica();

CREATE TRIGGER trg_auditoria_pedidos
AFTER INSERT OR UPDATE OR DELETE ON pedidos
FOR EACH ROW EXECUTE FUNCTION fn_auditoria_generica();

Notificaciones en tiempo real con pg_notify

-- PostgreSQL: publicar un evento cuando se inserta un pedido nuevo
-- Aplicaciones que escuchan este canal (usando LISTEN) recibirán la notificación
CREATE OR REPLACE FUNCTION fn_notificar_pedido_nuevo()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    PERFORM pg_notify(
        'pedido_nuevo',
        json_build_object(
            'id',          NEW.id,
            'producto_id', NEW.producto_id,
            'cantidad',    NEW.cantidad,
            'total',       NEW.total
        )::TEXT
    );
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_notificar_pedido
AFTER INSERT ON pedidos
FOR EACH ROW EXECUTE FUNCTION fn_notificar_pedido_nuevo();

-- Una aplicación en Node.js escucha el canal:
-- client.query('LISTEN pedido_nuevo');
-- client.on('notification', (msg) => console.log('Nuevo pedido:', msg.payload));

Cuándo usar triggers y cuándo evitarlos

Úsalos cuando:

  • Necesitas garantizar que una acción ocurra siempre que cambie un dato, sin importar qué aplicación o cliente realice la modificación. Un trigger en la base de datos es la única garantía real de consistencia en este sentido.
  • Implementas auditorías de cambios que deben funcionar incluso si se accede directamente a la base de datos desde una herramienta de administración.
  • Calculas campos derivados que deben mantenerse sincronizados automáticamente.
  • Aplicas reglas de validación que la base de datos debe hacer cumplir independientemente de la capa de aplicación.

Evítalos cuando:

  • La lógica es compleja y difícil de depurar: los errores dentro de un trigger son más difíciles de rastrear que los errores en código de aplicación.
  • El trigger puede activar otro trigger, que activa otro, creando cadenas de efectos difíciles de predecir (ambos motores tienen límites a la profundidad de triggers anidados).
  • Tienes muchas operaciones de escritura en la tabla y el trigger añade latencia no aceptable. Los triggers se ejecutan de forma síncrona en la misma transacción, por lo que ralentizan cada escritura.
  • Tu equipo no está acostumbrado a trabajar con triggers: código invisible que "hace cosas" sin que nadie lo llame explícitamente puede generar bugs muy difíciles de diagnosticar si el equipo no sabe que existe.

Resumen

  • Un trigger es un bloque de código que se ejecuta automáticamente cuando ocurre un evento (INSERT, UPDATE, DELETE) en una tabla.
  • Se definen con un momento (BEFORE o AFTER) y tienen acceso a NEW (el valor nuevo) y OLD (el valor anterior).
  • En MySQL, la lógica va directamente en el cuerpo del trigger. En PostgreSQL, primero se define una función que devuelve TRIGGER y luego se asocia al trigger.
  • Los triggers BEFORE pueden modificar los valores antes de que se confirmen o cancelar la operación. Los triggers AFTER se ejecutan sobre datos ya confirmados.
  • PostgreSQL añade características adicionales: UPDATE OF columna para activarse solo cuando cambia una columna específica, cláusula WHEN para condiciones en el propio trigger, y pg_notify para notificaciones en tiempo real.
  • Son ideales para auditorías, validaciones de integridad y cálculos derivados que deben funcionar independientemente de la capa de aplicación. Úsalos con moderación: el código que se ejecuta de forma invisible puede ser difícil de depurar.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a