🐬 Curso SQL · Nivel N6 Avanzado

Bloque 6: Procedimientos y triggers

Lleva la lógica dentro del motor: las funciones almacenadas que calculan, los procedimientos almacenados que automatizan tareas, las variables con control de flujo y los triggers que vigilan la base de datos y actúan solos cuando algo cambia. Todo sobre un gimnasio con socios, clases y reservas.

⏱ ≈ 16 horas 📚 5 lecciones ✍️ 15 ejercicios guiados 📝 Examen del bloque + laboratorio
Lección 1 · Teoría

Funciones almacenadas

Una función almacenada es código SQL guardado en el servidor que recibe parámetros y devuelve un solo valor. Se usa dentro de un SELECT como cualquier otra función.

1.1 · Primer función

cuota anual de un socio
CREATE FUNCTION f_cuota_anual(socio_id INT)
RETURNS DECIMAL(8,2) DETERMINISTIC
BEGIN
  DECLARE resultado DECIMAL(8,2);
  SELECT cuota * 12 INTO resultado FROM socios WHERE id = socio_id;
  RETURN resultado;
END
RETURNS tipo — declara qué devuelve (INT, DECIMAL, VARCHAR…)
DETERMINISTIC — mismo input → mismo output (ayuda al motor a optimizar)
DECLARE — crea una variable local
SELECT … INTO — guarda un valor en la variable
RETURN — devuelve el resultado (siempre al final)

1.2 · Usar la función

como cualquier otra función
SELECT nombre,
       f_cuota_anual(id) AS cuota_anual
FROM   socios;
⚠️ Las funciones deben devolver un valor con RETURN. No puedes usarlas para INSERT/UPDATE/DELETE de forma fiable: para ese tipo de lógica existen los procedimientos.
🎉 La función devuelve, el procedimiento hace. Vamos a por ellos.
Lección 2 · Teoría

Procedimientos almacenados

Un procedimiento almacenado es una secuencia de sentencias guardada en el servidor que se invoca con CALL. A diferencia de la función, no devuelve un valor: hace trabajo (insertar, validar, actualizar…).

2.1 · Crear con parámetros IN

da de alta un socio validando el email
CREATE PROCEDURE p_crear_socio(
  IN p_nombre VARCHAR(80),
  IN p_email  VARCHAR(90),
  IN p_cuota  DECIMAL(6,2)
)
BEGIN
  IF p_email IN (SELECT email FROM socios) THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'El email ya está registrado';
  ELSE
    INSERT INTO socios (nombre, email, cuota, alta, activo)
    VALUES (p_nombre, p_email, IFNULL(p_cuota, 39.90), CURDATE(), 1);
  END IF;
END
IN — parámetro de entrada (el valor entra, no sale)
OUT — el procedimiento devuelve un valor por ese parámetro
INOUT — entra un valor y sale otro
SIGNAL SQLSTATE — lanza un error controlado con mensaje

2.2 · Invocar con CALL

ejecutar el procedimiento
CALL p_crear_socio('Ana Rojo', 'ana.rojo@mail.com', 49.90);
CALL p_crear_socio('Dup', 'ana.rojo@mail.com', 39.90); -- error controlado
💡 Nombres: prefijo p_ para procedimientos, f_ para funciones y trg_ para triggers. Y los parámetros con prefijo p_ para no chocar con columnas del mismo nombre.
🎉 El servidor hace el trabajo: centraliza la lógica en un solo sitio. Ahora las variables y el control de flujo.
Lección 3 · Teoría

Variables y control de flujo

Dentro de un bloque BEGIN…END puedes declarar variables con DECLARE y tomar decisiones con IF, CASE, WHILE y REPEAT.

3.1 · DECLARE y SELECT INTO

almacenar un valor de la BD
CREATE PROCEDURE p_detalles_socio(IN p_id INT)
BEGIN
  DECLARE v_nombre VARCHAR(80);
  DECLARE v_cuota  DECIMAL(6,2);

  SELECT nombre, cuota INTO v_nombre, v_cuota
  FROM socios WHERE id = p_id;

  SELECT v_nombre AS socio, v_cuota AS cuota;
END

3.2 · IF / ELSE y CASE

categoría según cuota
CREATE PROCEDURE p_categoria(IN p_id INT)
BEGIN
  DECLARE v_cuota DECIMAL(6,2);
  DECLARE v_cat   VARCHAR(20);

  SELECT cuota INTO v_cuota FROM socios WHERE id = p_id;

  IF v_cuota < 45 THEN
    SET v_cat = 'Base';
  ELSEIF v_cuota < 55 THEN
    SET v_cat = 'Premium';
  ELSE
    SET v_cat = 'VIP';
  END IF;

  SELECT v_cat AS categoria;
END
⚠️ SET variable = valor asigna una variable; SELECT … INTO variable rellena desde la base de datos. Son dos formas distintas de llenar una variable.

3.3 · Errores controlados

validar antes de escribir
IF p_id IS NULL THEN
  SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Id inválido';
END IF;
💡 SQLSTATE '45000' es el código genérico para errores propios. El mensaje va en MESSAGE_TEXT y llega tal cual a quien llama.
🎉 Controlas el flujo. Ahora lo que vigila por ti: los triggers.
Lección 4 · Teoría

Triggers

Un trigger es un bloque que el motor ejecuta automáticamente cuando ocurre un evento en una tabla: INSERT, UPDATE o DELETE. Sirve para validar, auditar o sincronizar sin tocar la aplicación.

4.1 · Anatomía y primeros pasos

auditar bajas de socios
CREATE TRIGGER trg_socio_baja
AFTER UPDATE ON socios
FOR EACH ROW
BEGIN
  IF OLD.activo = 1 AND NEW.activo = 0 THEN
    INSERT INTO log_socios (socio_id, detalle)
    VALUES (NEW.id, CONCAT('Baja de ', NEW.nombre));
  END IF;
END
ZonaQué significa
AFTER/BEFORE UPDATE ON tablaCuándo se dispara y sobre qué tabla
FOR EACH ROWSe ejecuta por cada fila afectada
OLD.activoEl valor antes del cambio (en UPDATE/DELETE)
NEW.activoEl valor después del cambio (en UPDATE/INSERT)

4.2 · BEFORE para validar

impedir una cuota negativa
CREATE TRIGGER trg_cuota_no_negativa
BEFORE UPDATE ON socios
FOR EACH ROW
BEGIN
  IF NEW.cuota < 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'La cuota no puede ser negativa';
  END IF;
END
⚠️ BEFORE puede frenar la operación (con SIGNAL); AFTER ya no puede, solo reacciona a lo hecho (lo típico: auditar). Para «reglas de negocio» usa BEFORE; para «registro de actividad» usa AFTER.
💡 Un trigger BIEN usado evita bugs; un trigger ABUSADO esconde lógica y atrapa a todo el mundo. Piensa: ¿esta regla es del negocio (debería estar en la app) o de los datos (aquí)?
🎉 Elfos vigilantes trabajan por ti. Ahora el detalle técnico que hace posible todo esto: DELIMITER.
Lección 5 · Teoría

DELIMITER y buenas prácticas

Los bloques BEGIN…END contienen varias sentencias separadas por ;. Si el cliente usara ; como separador de comandos, partiría el bloque por la mitad. La solución es cambiar el delimitador mientras se define el objeto.

5.1 · DELIMITER en acción

crear objeto dentro de $$ … $$
DELIMITER $$

CREATE FUNCTION f_ejemplo() RETURNS INT DETERMINISTIC
BEGIN
  DECLARE v INT;
  SET v = 1;
  RETURN v;
END$$

DELIMITER ;

Con DELIMITER $$ el cliente ya no parte por ;; entrega el bloque entero al motor cuando ve $$. Luego se restaura DELIMITER ; para el resto del fichero.

⚠️ El DELIMITER lo entiende el cliente (Workbench, la terminal), no el servidor. En la práctica solo hace falta cuando el objeto lleva BEGIN…END con varias sentencias.

5.2 · Buenas prácticas

Prefijos f_, p_, trg_ y parámetros con p_
Validar con SIGNAL + mensaje claro antes de escribir
Programar un error posible deja la operación; decide si usar transacciones
Documentar con comentarios -- al inicio de cada objeto
Eliminar objetos con DROP FUNCTION / PROCEDURE / TRIGGER al recargar un script

5.3 · Limpiar antes de recargar

borrar objetos si ya existen
DROP FUNCTION IF EXISTS f_cuota_anual;
DROP PROCEDURE IF EXISTS p_crear_socio;
DROP TRIGGER IF EXISTS trg_socio_baja;
🎉 Bloque 6 completado: funciones, procedimientos, variables, triggers y DELIMITER. Ahora machaca los 15 ejercicios con el gimnasio. 🐬
Manos a la obra

✍️ Los 15 ejercicios del bloque

Abre MySQL Workbench y ejecuta sobre la base tw_gimnasio. Escribe los bloques entre DELIMITER $$ … $$ cuando hagan falta e incluye un comentario -- con el número del ejercicio. Recuerda: el fichero de la base ya trae objetos de ejemplo que puedes usar de guía.

📥 ¿No tienes la base de datos? Descárgala: sql/sql-bloque6_gimnasio.sql   y cárgala con mysql -u root -p < sql/sql-bloque6_gimnasio.sql.

Sección A · Funciones

1 Cuota duplicada

Crea la función f_cuota_doble(socio_id INT) que devuelva la cuota * 2 como DECIMAL(8,2).

Ver solución
-- Ejercicio 1: cuota doble
DELIMITER $$
CREATE FUNCTION f_cuota_doble(socio_id INT)
RETURNS DECIMAL(8,2) DETERMINISTIC
BEGIN
  DECLARE resultado DECIMAL(8,2);
  SELECT cuota * 2 INTO resultado FROM socios WHERE id = socio_id;
  RETURN resultado;
END$$
DELIMITER ;

2 Llamar a tu función

Consulta que muestre nombre y f_cuota_doble(id) con el alias cuota_doble para todos los socios.

Ver solución
-- Ejercicio 2: usar la función
SELECT nombre,
       f_cuota_doble(id) AS cuota_doble
FROM   socios;

3 Función con IFNULL

Crea f_antiguedad_dias(socio_id INT, fecha_ref DATE) que devuelva DATEDIFF(fecha_ref, alta) y, si el socio no existe, -1 con IFNULL.

Ver solución
-- Ejercicio 3: antigüedad con fallback
DELIMITER $$
CREATE FUNCTION f_antiguedad_dias(socio_id INT, fecha_ref DATE)
RETURNS INT DETERMINISTIC
BEGIN
  DECLARE resultado INT;
  SELECT DATEDIFF(fecha_ref, alta) INTO resultado
  FROM socios WHERE id = socio_id;
  RETURN IFNULL(resultado, -1);
END$$
DELIMITER ;

4 Función DETERMINISTIC

Explica con un comentario y crea una función f_descuento(precio DECIMAL) que devuelva precio * 0.90 declarada DETERMINISTIC.

Ver solución
-- Ejercicio 4: función pura y determinista
DELIMITER $$
CREATE FUNCTION f_descuento(precio DECIMAL(8,2))
RETURNS DECIMAL(8,2) DETERMINISTIC
BEGIN
  RETURN precio * 0.90;
END$$
DELIMITER ;

Sección B · Procedimientos

5 Insertar clase

Crea el procedimiento p_crear_clase con IN p_nombre, p_monitor y p_plazas que inserte en clases.

Ver solución
-- Ejercicio 5: dar de alta una clase
DELIMITER $$
CREATE PROCEDURE p_crear_clase(
  IN p_nombre  VARCHAR(60),
  IN p_monitor VARCHAR(60),
  IN p_plazas  TINYINT
)
BEGIN
  INSERT INTO clases (nombre, monitor, plazas)
  VALUES (p_nombre, p_monitor, p_plazas);
END$$
DELIMITER ;

6 Invocar con CALL

Usa CALL para crear la clase 'Zumba' con monitor 'Elena Prado' y 15 plazas.

Ver solución
-- Ejercicio 6: ejecutar el procedimiento
CALL p_crear_clase('Zumba', 'Elena Prado', 15);

7 Consulta dentro del procedimiento

El procedimiento p_listar_clases (sin parámetros) que haga SELECT * FROM clases.

Ver solución
-- Ejercicio 7: procedimiento que consulta
DELIMITER $$
CREATE PROCEDURE p_listar_clases()
BEGIN
  SELECT id, nombre, monitor, plazas FROM clases;
END$$
DELIMITER ;
CALL p_listar_clases();

8 Parámetro OUT

Crea p_total_socios(OUT p_total INT) que guarde COUNT(*) en el parámetro OUT con SELECT COUNT(*) INTO p_total.

Ver solución
-- Ejercicio 8: devolver con OUT
DELIMITER $$
CREATE PROCEDURE p_total_socios(OUT p_total INT)
BEGIN
  SELECT COUNT(*) INTO p_total FROM socios;
END$$
DELIMITER ;
CALL p_total_socios(@total);
SELECT @total AS total_socios;

Sección C · Control de flujo

9 IF / ELSEIF

Procedimiento p_etiqueta(p_id INT) que según cuota asigne 'Base' (< 45), 'Premium' (< 55) o 'VIP' y lo muestre.

Ver solución
-- Ejercicio 9: etiqueta con IF
DELIMITER $$
CREATE PROCEDURE p_etiqueta(IN p_id INT)
BEGIN
  DECLARE v_cuota DECIMAL(6,2);
  DECLARE v_eti  VARCHAR(20);
  SELECT cuota INTO v_cuota FROM socios WHERE id = p_id;
  IF v_cuota < 45 THEN
    SET v_eti = 'Base';
  ELSEIF v_cuota < 55 THEN
    SET v_eti = 'Premium';
  ELSE
    SET v_eti = 'VIP';
  END IF;
  SELECT v_eti AS etiqueta;
END$$
DELIMITER ;

10 Error controlado

Procedimiento p_validar_plazas(p_plazas INT) que lance SIGNAL SQLSTATE '45000' si p_plazas <= 0, y si es válido muestre SELECT p_plazas.

Ver solución
-- Ejercicio 10: validar con SIGNAL
DELIMITER $$
CREATE PROCEDURE p_validar_plazas(IN p_plazas INT)
BEGIN
  IF p_plazas <= 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Las plazas deben ser positivas';
  ELSE
    SELECT p_plazas AS plazas_validas;
  END IF;
END$$
DELIMITER ;

Sección D · Triggers

11 Trigger AFTER INSERT

Crea trg_clase_nueva: AFTER INSERT ON clases que inserte en log_socios la línea 'Nueva clase: ' concatenada con NEW.nombre.

Ver solución
-- Ejercicio 11: auditar alta de clase
DELIMITER $$
CREATE TRIGGER trg_clase_nueva
AFTER INSERT ON clases
FOR EACH ROW
BEGIN
  INSERT INTO log_socios (socio_id, detalle)
  VALUES (NEW.id, CONCAT('Nueva clase: ', NEW.nombre));
END$$
DELIMITER ;

12 Trigger con OLD y NEW

Crea trg_socio_baja: en AFTER UPDATE ON socios, si OLD.activo = 1 y NEW.activo = 0, registra la baja en log_socios.

Ver solución
-- Ejercicio 12: registrar bajas
DELIMITER $$
CREATE TRIGGER trg_socio_baja
AFTER UPDATE ON socios
FOR EACH ROW
BEGIN
  IF OLD.activo = 1 AND NEW.activo = 0 THEN
    INSERT INTO log_socios (socio_id, detalle)
    VALUES (NEW.id, CONCAT('Baja de ', NEW.nombre));
  END IF;
END$$
DELIMITER ;

13 Trigger BEFORE con validación

Crea trg_cuota_no_negativa: BEFORE UPDATE ON socios que lance error si NEW.cuota < 0.

Ver solución
-- Ejercicio 13: impedir cuotas negativas
DELIMITER $$
CREATE TRIGGER trg_cuota_no_negativa
BEFORE UPDATE ON socios
FOR EACH ROW
BEGIN
  IF NEW.cuota < 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'La cuota no puede ser negativa';
  END IF;
END$$
DELIMITER ;

Sección E · DELIMITER y limpieza

14 DELIMITER $$ … $$

Crea la función f_cuota_anual(socio_id INT) (cuota · 12) dentro de un bloque DELIMITER, con RETURNS DECIMAL(8,2) DETERMINISTIC.

Ver solución
-- Ejercicio 14: función con DELIMITER
DELIMITER $$
CREATE FUNCTION f_cuota_anual(socio_id INT)
RETURNS DECIMAL(8,2) DETERMINISTIC
BEGIN
  DECLARE resultado DECIMAL(8,2);
  SELECT cuota * 12 INTO resultado FROM socios WHERE id = socio_id;
  RETURN resultado;
END$$
DELIMITER ;

15 DROP de objetos

Escribe un guion que borre IF EXISTS la función f_cuota_anual, el procedimiento p_crear_socio y el trigger trg_socio_baja.

Ver solución
-- Ejercicio 15: limpiar objetos
DROP FUNCTION IF EXISTS f_cuota_anual;
DROP PROCEDURE IF EXISTS p_crear_socio;
DROP TRIGGER IF EXISTS trg_socio_baja;

16 🏆 Proyecto integrador: el paquete del gimnasio

Crea el fichero paquete_gimnasio.sql con las piezas que harían funcionar el alta de socios del gimnasio «solo con SQL». Requisitos:

  • Un procedimiento p_crear_socio con IN parámetros nombre, email y cuota.
  • Que valide el email con IF … THEN SIGNAL SQLSTATE '45000' si ya existe.
  • Que inserte el socio con INSERT INTO socios.
  • Un DELIMITER $$ … $$ alrededor de cada objeto.
  • Una función f_cuota_anual(socio_id).
  • Un trigger trg_socio_baja que audite las bajas en log_socios con OLD/NEW.
Ver una solución posible
-- Informe: paquete de alta y baja de socios
DELIMITER $$

CREATE PROCEDURE p_crear_socio(
  IN p_nombre VARCHAR(80),
  IN p_email  VARCHAR(90),
  IN p_cuota  DECIMAL(6,2)
)
BEGIN
  IF p_email IN (SELECT email FROM socios) THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'El email ya está registrado';
  ELSE
    INSERT INTO socios (nombre, email, cuota, alta, activo)
    VALUES (p_nombre, p_email, IFNULL(p_cuota, 39.90), CURDATE(), 1);
  END IF;
END$$

CREATE FUNCTION f_cuota_anual(socio_id INT)
RETURNS DECIMAL(8,2) DETERMINISTIC
BEGIN
  DECLARE resultado DECIMAL(8,2);
  SELECT cuota * 12 INTO resultado FROM socios WHERE id = socio_id;
  RETURN resultado;
END$$

CREATE TRIGGER trg_socio_baja
AFTER UPDATE ON socios
FOR EACH ROW
BEGIN
  IF OLD.activo = 1 AND NEW.activo = 0 THEN
    INSERT INTO log_socios (socio_id, detalle)
    VALUES (NEW.id, CONCAT('Baja de ', NEW.nombre));
  END IF;
END$$

DELIMITER ;
Evaluación del bloque

📝 Examen del Bloque 6

Dos partes: un test teórico autoevaluable (mínimo 70 %) y un reto práctico de autocorrección con laboratorio.

Parte A · Test teórico (aprueba con 11/15)

1 · ¿Qué devuelve SIEMPRE una función almacenada?
2 · ¿Cómo se invoca un procedimiento almacenado?
3 · Un parámetro IN en un procedimiento…
4 · ¿Qué declaración crea una variable local?
5 · ¿Con qué rellenas una variable desde una consulta?
6 · ¿Qué lanza un error controlado en MySQL?
7 · Un trigger se ejecuta…
8 · En un trigger, ¿qué referencia el valor ANTES del cambio?
9 · Un trigger AFTER UPDATE…
10 · Un trigger BEFORE se usa sobre todo para…
11 · ¿Para qué sirve DELIMITER $$?
12 · ¿Dónde entiende el DELIMITER?
13 · ¿Cuál es un buen prefijo para un trigger?
14 · ¿Qué significa DETERMINISTIC en una función?
15 · ¿Cómo se evita un error al recargar un guion con objetos?

Parte B · Reto «paquete del gimnasio»

Crea paquete_gimnasio.sql: procedimiento de alta de socio con validación de email, función de cuota anual y trigger de bajas con OLD/NEW, envuelto en DELIMITER. Tu SQL no se ejecuta en el laboratorio: se valida con reglas automáticas. Mínimo para aprobar: 6 de 8 casillas.

Cumplidos: 0 / 8

🎉 ¿Superaste las dos partes? Enhorabuena: el motor ya trabaja contigo, no para ti. En el bloque 7 harás que todo vuele: rendimiento, índices y funciones de ventana. 🐬🔥