CarlosSeijas
← Volver a los blogs

¿Qué son los Triggers en PostgreSQL? Guía práctica

Código
PostgreSQLTriggersBase de datosSQLDesarrolloBackendProgramación
¿Qué son los Triggers en PostgreSQL? Guía práctica

Triggers: los vigilantes automáticos de tu base de datos

Los triggers en PostgreSQL son como tener un asistente súper obediente que está 24/7 vigilando tu base de datos y ejecutando tareas automáticamente.

Imagínate que tienes un sistema de inventario y cada vez que alguien compra algo, quieres que automáticamente se reduzca el stock, se actualice la fecha de última venta, y si el inventario está bajo, que se envíe una alerta. Los triggers hacen todo esto sin que tengas que acordarte de programarlo cada vez. Es como magia, pero con SQL.

¿Cómo funcionan estos "vigilantes"?

Un trigger es básicamente un "si esto pasa, haz aquello" automatizado. Es como tener un sensor de movimiento en tu casa - cuando detecta algo específico, se activa automáticamente.

Los eventos que pueden despertar a tu trigger:

El timing perfecto:

Mi primer trigger (y por qué me emocioné tanto)

La primera vez que creé un trigger me sentí como un mago. Te voy a mostrar exactamente lo que hice - un ejemplo súper simple que me ayudó a entender el concepto.

Tenía una tabla de usuarios y quería que cada vez que se creara o modificara un usuario, se actualizara automáticamente la fecha de modificación. Súper básico, pero efectivo.

Paso 1: Las tablas que necesitamos

-- Tabla de usuarios
CREATE TABLE usuarios (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    fecha_actualizacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de logs para auditoría
CREATE TABLE logs_usuarios (
    id SERIAL PRIMARY KEY,
    usuario_id INTEGER,
    accion VARCHAR(20),
    datos_anteriores JSONB,
    datos_nuevos JSONB,
    fecha_log TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Paso 2: La función que hace la magia

-- Función que se ejecutará cuando se active el trigger
CREATE OR REPLACE FUNCTION actualizar_fecha_modificacion()
RETURNS TRIGGER AS $$
BEGIN
    -- Actualizar fecha de modificación
    NEW.fecha_actualizacion = CURRENT_TIMESTAMP;
    
    -- Insertar en la tabla de logs
    INSERT INTO logs_usuarios (usuario_id, accion, datos_anteriores, datos_nuevos)
    VALUES (
        NEW.id,
        TG_OP,
        CASE WHEN TG_OP = 'UPDATE' THEN row_to_json(OLD) ELSE NULL END,
        row_to_json(NEW)
    );
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Paso 3: Crear el trigger (mi momento favorito)

-- Trigger que se ejecuta ANTES de INSERT o UPDATE
CREATE TRIGGER trigger_actualizar_usuario
    BEFORE INSERT OR UPDATE ON usuarios
    FOR EACH ROW
    EXECUTE FUNCTION actualizar_fecha_modificacion();

La prueba de fuego

-- Insertar un usuario
INSERT INTO usuarios (nombre, email) 
VALUES ('Carlos Seijas', 'carlos@ejemplo.com');

-- Actualizar el usuario
UPDATE usuarios 
SET nombre = 'Carlos Seijas Dev' 
WHERE email = 'carlos@ejemplo.com';

-- Ver los logs generados
SELECT * FROM logs_usuarios;

Cuando vi que funcionaba automáticamente, literal me quedé como "¡¿cómo es que no sabía de esto antes?!"

Los dos tipos de triggers que debes conocer

1. Triggers de Fila (Row-level) - Los detallistas

Estos se ejecutan una vez por cada fila que se modifica. Si actualizas 1000 filas, se ejecuta 1000 veces:

-- Trigger que se ejecuta por cada fila
CREATE TRIGGER trigger_auditoria_productos
    AFTER INSERT OR UPDATE OR DELETE ON productos
    FOR EACH ROW
    EXECUTE FUNCTION auditoria_productos();

2. Triggers de Declaración (Statement-level) - Los eficientes

Estos se ejecutan una vez por declaración SQL, sin importar cuántas filas toques:

-- Trigger que se ejecuta por cada declaración
CREATE TRIGGER trigger_log_operacion
    AFTER INSERT OR UPDATE OR DELETE ON productos
    FOR EACH STATEMENT
    EXECUTE FUNCTION log_operacion_productos();

Un ejemplo que me salvó en producción

Te voy a contar de un sistema de inventario que implementé con triggers. Era para una tienda online y necesitaba que el stock se actualizara automáticamente con cada venta, plus generar alertas cuando el inventario estuviera bajo.

Las tablas del sistema

-- Tabla de productos
CREATE TABLE productos (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    stock INTEGER NOT NULL DEFAULT 0,
    precio DECIMAL(10,2) NOT NULL,
    stock_minimo INTEGER DEFAULT 10,
    fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de ventas
CREATE TABLE ventas (
    id SERIAL PRIMARY KEY,
    producto_id INTEGER REFERENCES productos(id),
    cantidad INTEGER NOT NULL,
    precio_unitario DECIMAL(10,2) NOT NULL,
    total DECIMAL(10,2),
    fecha_venta TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de alertas
CREATE TABLE alertas (
    id SERIAL PRIMARY KEY,
    mensaje TEXT NOT NULL,
    tipo VARCHAR(20) NOT NULL,
    fecha_alerta TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    resuelto BOOLEAN DEFAULT FALSE
);

La función que maneja todo automáticamente

CREATE OR REPLACE FUNCTION procesar_venta()
RETURNS TRIGGER AS $$
BEGIN
    -- Calcular el total de la venta
    NEW.total = NEW.cantidad * NEW.precio_unitario;
    
    -- Reducir el stock del producto
    UPDATE productos 
    SET stock = stock - NEW.cantidad
    WHERE id = NEW.producto_id;
    
    -- Verificar si el stock está bajo
    IF (SELECT stock FROM productos WHERE id = NEW.producto_id) < 
       (SELECT stock_minimo FROM productos WHERE id = NEW.producto_id) THEN
        
        INSERT INTO alertas (mensaje, tipo)
        VALUES (
            'Stock bajo para producto ID: ' || NEW.producto_id || 
            '. Stock actual: ' || (SELECT stock FROM productos WHERE id = NEW.producto_id),
            'STOCK_BAJO'
        );
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

El trigger que lo hace posible

CREATE TRIGGER trigger_procesar_venta
    BEFORE INSERT ON ventas
    FOR EACH ROW
    EXECUTE FUNCTION procesar_venta();

Probando que funcione

-- Insertar productos
INSERT INTO productos (nombre, stock, precio, stock_minimo) VALUES
('Laptop HP', 50, 999.99, 5),
('Mouse Logitech', 100, 29.99, 10),
('Teclado Mecánico', 25, 89.99, 5);

-- Hacer una venta que no genere alerta
INSERT INTO ventas (producto_id, cantidad, precio_unitario) 
VALUES (1, 2, 999.99);

-- Hacer una venta que genere alerta de stock bajo
INSERT INTO ventas (producto_id, cantidad, precio_unitario) 
VALUES (1, 44, 999.99);

-- Ver las alertas generadas
SELECT * FROM alertas;

-- Ver el stock actualizado
SELECT * FROM productos;

Este sistema me ahorró horas de debugging porque todo se manejaba automáticamente. Era imposible que se me olvidara actualizar el stock o generar las alertas.

PostgreSQL vs MySQL: la diferencia que importa

Si vienes de MySQL (como me pasó a mí), hay diferencias importantes que necesitas conocer:

CaracterísticaPostgreSQLMySQL
LenguajePL/pgSQL (más potente)SQL básico
Funciones separadas✅ Sí, obligatorio❌ No, código inline
TRUNCATE triggers✅ Soportado❌ No soportado
Variables OLD/NEW✅ Completo✅ Completo
Triggers en vistas✅ INSTEAD OF❌ No soportado

El mismo trigger en MySQL sería mucho más simple:

-- En MySQL sería así (más simple pero menos flexible)
DELIMITER //
CREATE TRIGGER actualizar_fecha_modificacion
    BEFORE UPDATE ON usuarios
    FOR EACH ROW
BEGIN
    SET NEW.fecha_actualizacion = CURRENT_TIMESTAMP;
END//
DELIMITER ;

En mi experiencia, PostgreSQL te da mucho más poder y flexibilidad, pero MySQL es más directo para cosas simples.

Triggers avanzados (donde se pone interesante)

1. Trigger con validaciones inteligentes

CREATE OR REPLACE FUNCTION validar_precio_producto()
RETURNS TRIGGER AS $$
BEGIN
    -- Solo ejecutar si el precio cambió
    IF OLD.precio IS DISTINCT FROM NEW.precio THEN
        -- Validar que el nuevo precio no sea negativo
        IF NEW.precio < 0 THEN
            RAISE EXCEPTION 'El precio no puede ser negativo';
        END IF;
        
        -- Validar que no aumente más del 50%
        IF NEW.precio > OLD.precio * 1.5 THEN
            RAISE EXCEPTION 'El precio no puede aumentar más del 50%% de una vez';
        END IF;
        
        -- Log del cambio de precio
        INSERT INTO logs_precios (producto_id, precio_anterior, precio_nuevo)
        VALUES (NEW.id, OLD.precio, NEW.precio);
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

2. Auditoría completa (mi favorito para compliance)

CREATE OR REPLACE FUNCTION auditoria_completa()
RETURNS TRIGGER AS $$
BEGIN
    -- Insertar en tabla de auditoría
    INSERT INTO auditoria (
        tabla_nombre,
        operacion,
        usuario_bd,
        fecha_hora,
        datos_anteriores,
        datos_nuevos
    ) VALUES (
        TG_TABLE_NAME,
        TG_OP,
        current_user,
        current_timestamp,
        CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) 
             WHEN TG_OP = 'UPDATE' THEN row_to_json(OLD) 
             ELSE NULL END,
        CASE WHEN TG_OP = 'DELETE' THEN NULL 
             ELSE row_to_json(NEW) END
    );
    
    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

3. Sincronización automática de datos

CREATE OR REPLACE FUNCTION sincronizar_datos()
RETURNS TRIGGER AS $$
BEGIN
    -- Actualizar tabla resumen
    UPDATE resumen_ventas 
    SET 
        total_ventas = total_ventas + NEW.total,
        ultima_actualizacion = CURRENT_TIMESTAMP
    WHERE producto_id = NEW.producto_id;
    
    -- Si no existe, crear nueva entrada
    IF NOT FOUND THEN
        INSERT INTO resumen_ventas (producto_id, total_ventas, ultima_actualizacion)
        VALUES (NEW.producto_id, NEW.total, CURRENT_TIMESTAMP);
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Lo que he aprendido a la mala (mejores prácticas)

1. Mantén la lógica simple (por favor)

-- ❌ Malo: Demasiada lógica compleja
CREATE OR REPLACE FUNCTION trigger_complejo()
RETURNS TRIGGER AS $$
BEGIN
    -- 100 líneas de código complejo aquí
    -- Múltiples consultas
    -- Lógica de negocio compleja
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- ✅ Bueno: Lógica simple y clara
CREATE OR REPLACE FUNCTION actualizar_timestamp()
RETURNS TRIGGER AS $$
BEGIN
    NEW.fecha_actualizacion = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

2. Maneja errores como un pro

CREATE OR REPLACE FUNCTION trigger_con_manejo_errores()
RETURNS TRIGGER AS $$
BEGIN
    BEGIN
        -- Operación que puede fallar
        INSERT INTO tabla_externa (datos) VALUES (NEW.info);
    EXCEPTION
        WHEN OTHERS THEN
            -- Log del error
            INSERT INTO logs_errores (mensaje, fecha) 
            VALUES (SQLERRM, CURRENT_TIMESTAMP);
            -- Continuar sin fallar
    END;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

3. Evita los loops infinitos (aprendí esto a la mala)

-- ❌ Peligro: Puede crear loop infinito
CREATE OR REPLACE FUNCTION trigger_peligroso()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE misma_tabla SET campo = 'valor' WHERE id = NEW.id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- ✅ Mejor: Usar condiciones
CREATE OR REPLACE FUNCTION trigger_seguro()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.campo != 'valor' THEN
        NEW.campo = 'valor';
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Debugging: cuando las cosas se ponen raras

Ver qué triggers tienes (súper útil)

-- Listar todos los triggers
SELECT 
    trigger_name,
    event_manipulation,
    event_object_table,
    action_timing,
    action_statement
FROM information_schema.triggers
WHERE trigger_schema = 'public';

-- Ver triggers de una tabla específica
SELECT * FROM information_schema.triggers 
WHERE event_object_table = 'usuarios';

Desactivar temporalmente (salvavidas en producción)

-- Desactivar trigger
ALTER TABLE usuarios DISABLE TRIGGER trigger_actualizar_usuario;

-- Reactivar trigger
ALTER TABLE usuarios ENABLE TRIGGER trigger_actualizar_usuario;

-- Desactivar todos los triggers de una tabla
ALTER TABLE usuarios DISABLE TRIGGER ALL;

Limpiar cuando ya no los necesites

-- Eliminar trigger
DROP TRIGGER trigger_actualizar_usuario ON usuarios;

-- Eliminar función (si no se usa en otros lugares)
DROP FUNCTION actualizar_fecha_modificacion();

Cuándo usar triggers (y cuándo NO)

✅ Úsalos para cosas como:

  1. Auditoría automática

    • Registrar quién cambió qué y cuándo
    • Cumplir con regulaciones (GDPR, SOX, etc.)
  2. Validaciones complejas

    • Reglas de negocio que involucran varias tablas
    • Validaciones que no puedes hacer con constraints simples
  3. Sincronización de datos

    • Mantener tablas de resumen actualizadas
    • Replicar datos entre sistemas
  4. Logging automático

    • Registrar operaciones críticas sin falta
    • Generar alertas cuando pase algo importante

❌ Evítalos para:

  1. Lógica de negocio súper compleja

    • Es más fácil debuggear en la aplicación
    • Los triggers son difíciles de mantener
  2. Operaciones que necesitan rollback

    • Los triggers no pueden deshacer cosas externas
    • Mejor en transacciones de tu app
  3. Operaciones súper lentas

    • Pueden hacer que tu base de datos se vuelva lenta
    • Mejor usar jobs asíncronos

Mi reflexión después de años usando triggers

Mira, los triggers son increíblemente poderosos y los he usado en proyectos complejos donde me han salvado la vida. Pero también he visto proyectos donde se volvieron una pesadilla por usarlos mal.

Lo importante es entender que son una herramienta, no una solución mágica. En mi experiencia, si te encuentras escribiendo más de 20-30 líneas en un trigger, probablemente deberías considerar mover esa lógica a tu aplicación.

Mi consejo para empezar: Comienza con triggers súper simples - timestamp automático, logging básico, validaciones sencillas. Una vez que te sientas cómodo y entiendas cómo funcionan, entonces puedes explorar casos más complejos.

Y ojo, siempre documenta qué hacen tus triggers. Tu yo del futuro (y tus compañeros de equipo) te lo van a agradecer.

Mi conclusión personal

Los triggers en PostgreSQL son una herramienta súper poderosa cuando se usan correctamente. Con PL/pgSQL tienes muchísimo más poder que en MySQL para crear lógica compleja y manejo de errores robusto.

Pero recuerda: con gran poder viene gran responsabilidad. Úsalos con moderación, siempre pensando en el rendimiento y la mantenibilidad. Son perfectos para auditoría, validaciones y sincronización básica, pero mantén la lógica de negocio heavy en tu aplicación.

¿Ya has experimentado con triggers en PostgreSQL? ¿Tienes algún caso de uso interesante o algún horror story que compartir? Me encantaría escuchar tu experiencia - cada historia de triggers me enseña algo nuevo.

Comentarios

Posts relacionados