¿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:
- INSERT: "Ey, metieron algo nuevo en la tabla"
- UPDATE: "Oye, cambiaron algo que ya existía"
- DELETE: "Ups, borraron algo"
- TRUNCATE: "¡Vaciaron toda la tabla!" (esto es específico de PostgreSQL)
El timing perfecto:
- BEFORE: "Hazlo antes de que pase el evento"
- AFTER: "Hazlo después de que ya pasó"
- INSTEAD OF: "En lugar de hacer eso, haz esto" (solo para vistas)
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ística | PostgreSQL | MySQL |
---|---|---|
Lenguaje | PL/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:
-
Auditoría automática
- Registrar quién cambió qué y cuándo
- Cumplir con regulaciones (GDPR, SOX, etc.)
-
Validaciones complejas
- Reglas de negocio que involucran varias tablas
- Validaciones que no puedes hacer con constraints simples
-
Sincronización de datos
- Mantener tablas de resumen actualizadas
- Replicar datos entre sistemas
-
Logging automático
- Registrar operaciones críticas sin falta
- Generar alertas cuando pase algo importante
❌ Evítalos para:
-
Lógica de negocio súper compleja
- Es más fácil debuggear en la aplicación
- Los triggers son difíciles de mantener
-
Operaciones que necesitan rollback
- Los triggers no pueden deshacer cosas externas
- Mejor en transacciones de tu app
-
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

¿Qué es SQL? Descubre como hablar con las Bases de Datos
Explora qué es SQL, su historia, importancia, componentes básicos y cómo se relaciona con las bases de datos. Una introducción teórica a la lengua franca de los datos.

Map vs forEach en JavaScript: Cuándo y Cómo Utilizarlos
Diferencias entre map y forEach en JavaScript, con ejemplos prácticos y casos de uso