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
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
1.2 · Usar la función
SELECT nombre,
f_cuota_anual(id) AS cuota_anual
FROM socios;
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
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
2.2 · Invocar con CALL
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
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.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
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
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
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.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
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
| Zona | Qué significa |
|---|---|
AFTER/BEFORE UPDATE ON tabla | Cuándo se dispara y sobre qué tabla |
FOR EACH ROW | Se ejecuta por cada fila afectada |
OLD.activo | El valor antes del cambio (en UPDATE/DELETE) |
NEW.activo | El valor después del cambio (en UPDATE/INSERT) |
4.2 · BEFORE para validar
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.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
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.
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
f_, p_, trg_ y parámetros con p_SIGNAL + mensaje claro antes de escribir-- al inicio de cada objetoDROP FUNCTION / PROCEDURE / TRIGGER al recargar un script5.3 · Limpiar antes de recargar
DROP FUNCTION IF EXISTS f_cuota_anual;
DROP PROCEDURE IF EXISTS p_crear_socio;
DROP TRIGGER IF EXISTS trg_socio_baja;
✍️ 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.
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_sociocon 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_bajaque audite las bajas enlog_sociosconOLD/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 ;
📝 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 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