Diferencia entre revisiones de «BD UD7 Control de Flujo»

De MediaWiki
Ir a la navegación Ir a la búsqueda
(Ejercicios propuestos IF / ELSE)
(Instrucción CASE)
Línea 479: Línea 479:
  
  
 +
 +
<br />
 +
* Existe una [https://dev.mysql.com/doc/refman/8.0/en/control-flow-functions.html#operator_case función CASE] que se emplea para control de flujo y que tiene una sintaxis un poco diferente.
 +
: Este tipo de función se puede emplear en el interior de una sentencia SELECT de la forma:
 +
:::<syntaxhighlight lang="SQL" enclose="div" highlight="" >
 +
SELECT nombre, case anhos
 +
when 1 then 'Pequeño'
 +
                when 2 then 'Mediano'
 +
                else 'Grande'
 +
        end as anhos
 +
FROM ANIMALES;
 +
</syntaxhighlight>
  
  

Revisión del 10:19 2 jul 2020

Instrucción IF-ELSE


  • La sintaxis completa de esta instrucción está en este enlace.
Fijarse que el END IF debe llevar punto y coma si a continuación le siguen nuevas instrucciones.
Mod BD Prog ControlFlujo 1.jpg
Imagen obtenida de https://dev.mysql.com]


Su funcionamiento es el mismo que en cualquier lenguaje de programación.



  • No debemos confundir esta sentencia con unas funciones de control de flujo que incorpora Mysql y que pueden ser empleadas dentro de una sentencia SELECT.
Podéis consultarlas es este enlace.
Mod BD Prog ControlFlujo 2.jpg
Imagen obtenida de https://dev.mysql.com]


Por ejemplo, si queréis visualizar 'Desconocido' en un listado de atracciones cuando la fecha de inicio sea NULL, podrías poner la orden siguiente:
SELECT nombre, IFNULL(fecha_inicio,'Desconocido')
FROM ATRACCIONES
ORDER BY nombre;
Disponéis de funciones:
  • IF
  • IFNULL
  • CASE
  • NULLIF


  • Vamos a crear un procedimiento de nombre artistas_getPorNif que recupere los datos de un artista enviando un nif. En caso de que el artista no exista, devolverá la cadena 'NO ENCONTRADO' sin utilizar un parámetro de salida. La cadena que va a devolver tendrá el formato: artista>nif:nombre:apellidos-jefe>nif:nombre:apellidos
En caso de que no tenga jefe, el formato será: artista>nif:nombre:apellidos-jefe>SIN JEFE
Veremos que podemos emplear las funciones anteriores para simplificar el código...
Recordar que si un SELECT...INTO no se ejecuta, las variables mantienen sus valores por defecto...
USE CIRCO;
DROP PROCEDURE IF EXISTS artistas_getPorNif;
DELIMITER $$
CREATE PROCEDURE artistas_getPorNif(IN p_nif char(9))		
    COMMENT 'Devuelve la información de un artista con el siguiente formato: artista>nif:nombre:apellidos-jefe>nombre:apellidos'
BEGIN
    DECLARE v_nifArt,v_nifJefe char(9);		-- No ponemos default, por lo tanto el valor por defecto es NULL
    DECLARE v_nombreArt,v_nombreJefe varchar(45);
    DECLARE v_apellArt,v_apellJefe varchar(100);
    
    DECLARE v_cadenaArtista varchar(400);
    DECLARE v_cadenaJefe varchar(400);
    
    
    SELECT A.nif,A.nombre,A.apellidos,J.nif,J.nombre,J.apellidos
    INTO v_nifArt,v_nombreArt,v_apellArt,v_nifJefe,v_nombreJefe,v_apellJefe
    FROM ARTISTAS as A LEFT OUTER JOIN ARTISTAS as J 
				ON (A.nif_jefe = J.nif)
    WHERE A.nif=p_nif;
    
    if (v_nifArt IS NULL) THEN		-- No hay un artista con ese NIF
        SELECT 'NO ENCONTRADO';
    ELSE
   	SET v_cadenaArtista=CONCAT('artista>',v_nifArt,':',v_nombreArt,':',v_apellArt,'-jefe>');
        SET v_cadenaJefe = IFNULL(CONCAT(v_nifJefe,':',v_nombreJefe,':',v_apellJefe),'SIN JEFE');
        SELECT CONCAT(v_cadenaArtista,v_cadenaJefe);
    END IF;
    
END$$

Ejemplo de uso:

CALL artistas_getPorNif('11111111A');	-- Artista con jefe
CALL artistas_getPorNif('22222222B');	-- Artista sin jefe
CALL artistas_getPorNif('22222222X');	-- No existe Artista


Vamos a analizar un poco el código.
  • Lo primero es la sentencia que busca los datos del artista y el jefe. Esta sentencia es SQL que ya deberías saber.
Si se quisiera hacer de forma 'menos óptima', podríamos buscar primero los datos del artista y una vez guardado en una variable local el nif del jefe, comprobar si este era null para buscar o no los datos del jefe.
En la solución actual, hacemos uso del LEFT OUTER JOIN para que traiga todas las filas estén o no relacionadas con la tabla 'JEFES'.
  • Una vez recuperados los datos en variables locales, comprobamos con un IF si el v_nifArt es null. Fijarse que en la definición de la variable no tiene asignado valor por defecto y por tanto su valor por defecto es NULL. Si pusiéramos valor por defecto , en el IF tendríamos que utilizar la comparación v_nifArt='valor_por_defecto'.
  • En el caso de que encontrara al artista, tenemos que comprobar para crear la cadena de salida con el formato indicado, si tiene o no jefe. Podríamos emplear otra instrucción IF pero es mucho más cómodo emplear la función IFNULL. Si el jefe fuera null, la concatenación de cadenas daría NULL y por tanto dicha función devolvería el segundo parámetro (la cadena SIN JEFE).



  • Nota aclaratoria: Cuando se está desarrollando una aplicación, lo normal será que el control de que si existe un dato o no, lo programéis en el código de la aplicación y no en el procedimiento.
Es decir, en el ejercicio anterior, normalmente no se envían cadenas de tipo 'NO ENCONTRADO'. Se envían los datos con un SELECT:
    SELECT A.nif,A.nombre,A.apellidos,J.nif,J.nombre,J.apellidos
    INTO v_nifArt,v_nombreArt,v_apellArt,v_nifJefe,v_nombreJefe,v_apellJefe
    FROM ARTISTAS as A LEFT OUTER JOIN ARTISTAS as J 
				ON (A.nif_jefe = J.nif)
    WHERE A.nif=p_nif;
y es en la aplicación que recibe los datos en forma de conjunto de registros (RESULTSET) la que analiza los valores y muestra los mensajes de error al usuario...
Lo anterior sería correcto si el usuario va a llamar él mismo al procedimiento de forma directa.



  • Nota aclaratoria: Muchas veces, al escribir procedimientos almacenados necesitamos saber si una consulta ha encontrado algo o no.
En el ejemplo anterior lo supimos comprobando el valor variable v_nifArt y mirando si tenía el valor por defecto NULL, queriendo esto decir que la sentencia SELECT no encontró ningún resultado.
No siempre vamos a querer guardar los datos del SELECT en variables locales y por lo tanto no tendríamos la opción anterior de comprobar si existe una determinada fila.
En otros gestores, como SQL SERVER, tenéis la variable @@ROWCOUNT que devuelve el número de filas afectadas o leídas.
En Mysql, el equivalente a esa propiedad es la función FOUND_ROWS() pero esta función en la versión 8.0 está deprecated. Quiero esto decir que será eliminada en versiones posteriores.
Podemos hacer uso de la sentencia SELECT COUNT(*) dentro de un IF para determinar si existe o no un dato, de la forma:
IF (SELECT COUNT(*) FROM TABLA WHERE condiciones)>0 THEN
  -- Código en caso de cumplir la condición
END IF;
En este caso no queremos guardar nada de la consulta y sólo queremos saber si existe o no alguna fila (se podría emplear en el select cualquier expresión que devuelva una única fila y emplear cualquier tipo de comparación).
Pero si el uso que le queremos dar es el de recuperar información, si empleamos la forma anterior tendríamos que realizar otra vez la misma consulta, guardando la información que necesitemos.
En ese caso es mejor emplear el valor por defecto de la variabla que vamos a emplear para guardar el resultado de la consulta, para saber si existe o no (como hizimos antes):
DECLARE var varchar(50);  -- Valor por defecto null

SELECT nombre 
FROM TABLA 
WHERE condiciones 
INTO var;

IF (var is null) THEN
  -- Sabemos que no ha encontrado filas
END IF;




Ejercicios propuestos IF / ELSE

  • Ejercicio 1: Crea un procedimiento de nombre pistas_getListAnimales que devuelva los animales (nombre, peso y anhos) que trabajen en la pista indicada. En caso de que no haya animales deberá devolver SIN ANIMALES y en el caso de que no exista la pista, ESA PISTA NO EXISTE.


  • Ejercicio 2: Crea un procedimiento de nombre atracciones_getNumeroPorFecha que devuelva el número de atracciones que se celebraron en la fecha indicada. En caso de que no hubiera atracciones ese día debe devolver el número -1.
Haz dos variantes de este ejercicio. Una en la que la información se devuelva en un SELECT y otra en la que se emplee un parámetro de salida.
Nota: Como comenté antes, el devolver -1 sería el 'valor' que un programador chequearía en la aplicación cliente y en base a ese valor mostraría un mensaje al usuario.


  • Ejercicio 3: Crea un procedimiento de nombre animales_updateCuidador al que se le pase el nombre de un animal y si dicho animal no está cuidado por un artista (table ANIMALES_ARTISTAS) deben ser asignados al cuidador que cuide a menos animales. El procedimiento debe devolver con un parámetro de salida el nif del cuidador al que se le ha asignado al animal y en forma de registro (con un SELECT) a cuantos animales está cuidando, contando el actual, tiene asignados el cuidador. En caso de que el animal no exista, el nif tendrá de valor y el select mandará -1. En caso de que ya tenga cuidador, se devolverá el nif del cuidador actual y 0 en el SELECT.
Como un mismo animal puede ser cuidado por varios artistas, recuerda emplear LIMIT para coger el primero...
Añade un animal que no tenga asignado un artista.



  • Otros ejercicios propuestos sin solución:
  • Ejercicio 4: Crea un procedimiento de nombre atracciones_getCantidadPorFechas al que se le pase dos fechas y devuelva en forma de parámetro de salida, el número de atracciones celebradas entre las dos fechas indicadas (incluídas ambas). En caso de que el formato de las fechas no sea correcto (por ejemplo, enviamos 1990-45-66) o que la primera fecha sea mayor que la segunda, en el parámetro devolverá -1 y mostrará la cadena FECHAS INCORRECTAS. En el caso de que no haya atracciones entre las fechas indicadas el parámetro de salida debe devolver 0 y mostrar la cadena SIN ATRACCIONES ENTRE LAS FECHAS INDICADAS.
Para chequear que las fechas tengan un formato correcto podéis emplear la función DATE que aplicada a una fecha devolverá null si la fecha no tiene un formato correcto.


  • Ejercicio 5: Crea un procedimiento de nombre de nombre pistas_getListPorAforo al que se le pase un aforo y devuelva, empleando el mismo parámetro, cuantas pistas tienen un aforo superior al enviado. Al mismo tiempo debe mostrar los nombres de las pistas. En caso de que no haya pistas, debe devolver -1 y mostrar el texto SIN PISTAS.


  • Ejercicio 6: Crea un procedimiento de nombre artistas_getListPorAtraccion al que se le pase un nif de un artista y dos fechas y devuelva en forma de parámetro de salida, en cuantas atracciones trabajó ese artista entre las dos fechas indicadas.
En caso de que el artista no exista, el parámetro de salida debe devolver -1 y mostrar la cadena NO EXISTE ESE ARTISTA.
En caso de que las fechas no tenga un formato correcto, el parámetro de salida debe devolver -1 y mostrar la cadena FECHAS INCORRECTAS.
Si existe el artista y hay atracciones, el procedimiento también debe mostrar el nombre de las atracciones (sin repetirse) ordenadas alfabéticamente.






Solución Ejercicios propuestos IF / ELSE
  • Ejercicio 1: Crea un procedimiento de nombre pistas_getAnimales que devuelva los animales (nombre, peso y anhos) que trabajen en la pista indicada. En caso de que no haya animales deberá devolver SIN ANIMALES y en el caso de que no exista la pista, ESA PISTA NO EXISTE.
USE CIRCO;
DROP PROCEDURE IF EXISTS pistas_getListAnimales;
DELIMITER $$
CREATE PROCEDURE pistas_getListAnimales(p_nombrePista varchar(50))		
    COMMENT 'Devuelve los animales (nombre, peso y edad) que trabajen en la pista indicada. En caso de que no haya animales deberá devolver SIN ANIMALES y en el caso de que no exista la pista, ESA PISTA NO EXISTE.'
BEGIN
    DECLARE v_temp int;
    
    SELECT COUNT(*)     -- La función FOUND_ROWS() está deprecated.
    INTO v_temp
    FROM PISTAS
    WHERE nombre = p_nombrePista;
    
    if (v_temp=0) THEN
        SELECT 'ESA PISTA NO EXISTE';
    ELSE
        SELECT COUNT(*)
        INTO v_temp
        FROM ANIMALES
        WHERE nombre_pista = p_nombrePista;
        
        IF (v_temp=0) THEN
            SELECT 'SIN ANIMALES';
        ELSE
            SELECT nombre, peso, anhos
            FROM ANIMALES
            WHERE nombre_pista = p_nombrePista
            ORDER BY nombre;
        END IF;
    END IF;
    
END$$
DELIMITER ;


Una forma más óptima:
Como comenté antes, si no necesitamos guardar ningún dato, podemos emplear SELECT COUNT(*) dentro de un IF:
USE CIRCO;
DROP PROCEDURE IF EXISTS pistas_getListAnimales;
DELIMITER $$
CREATE PROCEDURE pistas_getListAnimales(p_nombrePista varchar(50))		
    COMMENT 'Devuelve los animales (nombre, peso y edad) que trabajen en la pista indicada. En caso de que no haya animales deberá devolver SIN ANIMALES y en el caso de que no exista la pista, ESA PISTA NO EXISTE.'
BEGIN
    DECLARE v_temp int;
    
    IF (SELECT COUNT(*)     -- La función FOUND_ROWS() está deprecated.
        FROM PISTAS
        WHERE nombre = p_nombrePista) = 0 THEN
    
        SELECT 'ESA PISTA NO EXISTE';
    ELSE
        IF (SELECT COUNT(*)
        FROM ANIMALES
        WHERE nombre_pista = p_nombrePista)=0 THEN
            SELECT 'SIN ANIMALES';
        ELSE
            SELECT nombre, peso, anhos
            FROM ANIMALES
            WHERE nombre_pista = p_nombrePista
            ORDER BY nombre;
        END IF;
    END IF;
    
END$$
DELIMITER ;


Ejemplo de uso:

CALL pistas_getListAnimales('NOEXISTE');		-- Pista que no existe
CALL pistas_getListAnimales('La grande');		-- Pista sin animales
CALL pistas_getListAnimales('LATERAL1');		-- Pista con animales


  • Ejercicio 2: Crea un procedimiento de nombre atracciones_getNumeroPorFecha que devuelva el número de atracciones que se celebraron en la fecha indicada. En caso de que no hubiera atracciones ese día debe devolver el número -1.
Haz dos variantes de este ejercicio. Una en la que la información se devuelva en un SELECT y otra en la que se emplee un parámetro de salida.
Nota: Como comenté antes, el devolver -1 sería el 'valor' que un programador chequearía en la aplicación cliente y en base a ese valor mostraría un mensaje al usuario.
USE CIRCO;
DROP PROCEDURE IF EXISTS atracciones_getNumeroPorFecha;
DELIMITER $$
CREATE PROCEDURE atracciones_getNumeroPorFecha(p_fecha date)		
    COMMENT 'Devuelve el número de atracciones que se celebraron en la fecha indicada. En caso de que no hubiera atracciones ese día debe devolver el número -1.'
BEGIN
   
    SELECT COUNT(*)
    INTO v_temp
    FROM ATRACCION_DIA
    WHERE fecha = p_fecha;
    
    if (v_temp=0) THEN
        SELECT -1;
    ELSE
        SELECT v_temp;
    END IF;
    
END$$
DELIMITER ;


Ejemplo de uso:

CALL atracciones_getNumeroPorFecha('2001-04-01');		-- Hay atracciones
CALL atracciones_getNumeroPorFecha('2001-05-01');		-- No hay atracciones


Variante empleando la función IF (la variante con parámetro de salida queda como ejercicio)

USE CIRCO;
DROP PROCEDURE IF EXISTS atracciones_getNumeroPorFecha;
DELIMITER $$
CREATE PROCEDURE atracciones_getNumeroPorFecha(p_fecha date)		
    COMMENT 'Devuelve el número de atracciones que se celebraron en la fecha indicada. En caso de que no hubiera atracciones ese día debe devolver el número -1.'
BEGIN
    DECLARE v_temp int;		
    
    SELECT IF(COUNT(*)>0,COUNT(*),-1)		-- Simplificamos el código con la función IF
    FROM ATRACCION_DIA
    WHERE fecha = p_fecha;

END$$
DELIMITER ;



  • Ejercicio 3: Crea un procedimiento de nombre animales_updateCuidador al que se le pase el nombre de un animal y si dicho animal no está cuidado por un artista (table ANIMALES_ARTISTAS) deben ser asignados al cuidador que cuide a menos animales. El procedimiento debe devolver con un parámetro de salida el nif del cuidador al que se le ha asignado al animal y en forma de registro (con un SELECT) a cuantos animales está cuidando, contando el actual, tiene asignados el cuidador. En caso de que el animal no exista, el nif tendrá de valor y el select mandará -1. En caso de que ya tenga cuidador, se devolverá el nif del cuidador actual y 0 en el SELECT.
USE CIRCO;
DROP PROCEDURE IF EXISTS animales_updateCuidador;
DELIMITER $$
CREATE PROCEDURE animales_updateCuidador(p_nombreAnimal varchar(50), OUT p_nifCuidador char(9))		
    COMMENT 'Asigna un cuidador (el que tenga menos animales asignados) a un animal. Devuelve el nif del cuidador'
BEGIN
    DECLARE v_numAnimalesAsignados int;
    
    SET p_nifCuidador = '';	-- Valor por defecto
    
    IF (SELECT COUNT(*)
        FROM ANIMALES
        WHERE nombre = p_nombreAnimal)=0 THEN       -- El animal no existe

        SELECT -1;
    ELSE
        SELECT nif_artista
        INTO p_nifCuidador
        FROM ANIMALES_ARTISTAS
        WHERE nombre_animal = p_nombreAnimal
        LIMIT 1;  -- Puede haber varios artistas cuidando de un mismo animal

        IF (p_nifCuidador = '') THEN  -- No tiene cuidador actual ya que no encontró cuidador y por tanto mantiene el valor asignado previamente
            -- Buscamos el cuidador que tiene menos animales asignados.
            SELECT nif_artista,COUNT(*) as numero
            INTO p_nifCuidador,v_numAnimalesAsignados
            FROM ANIMALES_ARTISTAS
            GROUP BY nif_artista
            ORDER BY numero ASC
            LIMIT 1;

            INSERT INTO ANIMALES_ARTISTAS (nombre_animal,nif_artista)
            VALUES (p_nombreAnimal,p_nifCuidador);
            
            SET v_numAnimalesAsignados = v_numAnimalesAsignados +1;
            SELECT v_numAnimalesAsignados;
            
        ELSE    -- Tiene cuidador y está guardado en p_nifCuidador
            SELECT 0;
        END IF;
    END IF;
    
END$$
DELIMITER ;


Ejemplo de uso:

CALL animales_updateCuidador('NO EXISTE',@nifCuidador);  -- Animal que no existe
SELECT @nifCuidador;

CALL animales_updateCuidador('Caiman',@nifCuidador);    -- Animal con cuidadores ya asignados
SELECT @nifCuidador;

CALL animales_updateCuidador('Sant',@nifCuidador);       -- Nuevo animal añadido de nombre Sant, se asigna al nif 44444444D
SELECT @nifCuidador;




Instrucción CASE

Fijarse que el END CASE debe llevar punto y coma si a continuación le siguen nuevas instrucciones.
Mod BD Prog ControlFlujo 3.jpg
  • La instrucción CASE viene a ser como un IF-ELSEIF-ELSEIF-ELSEIF... pero de una forma mucho más legible.


  • Veamos un ejemplo de uso.
Crea un procedimiento de nombre utilidades_getDia al que se le pase como parámetro un número entre 1 y 7 y devuelva, en forma de parámetro de salida, Lunes, Martes, Miércoles,....
En caso de que el número esté fuera de rango debe devolver la cadena null.


USE CIRCO;
DROP PROCEDURE IF EXISTS utilidades_getDia;
DELIMITER $$
CREATE PROCEDURE utilidades_getDia(p_numDia tinyint, OUT p_nombreDia varchar(9))		
    COMMENT 'Devuelve en p_nombreDia el nombre del día de la semana asociado al número indicado en p_numDia siendo el 1 Lunes'
BEGIN
		CASE p_numDia
			WHEN 1 THEN 
				SET p_nombreDia = 'LUNES';
			WHEN 2 THEN 
				SET p_nombreDia = 'MARTES';
			WHEN 3 THEN 
				SET p_nombreDia = 'MIERCOLES';
			WHEN 4 THEN 
				SET p_nombreDia = 'JUEVES';
			WHEN 5 THEN 
				SET p_nombreDia = 'VIERNES';
			WHEN 6 THEN 
				SET p_nombreDia = 'SABADO';
			WHEN 7 THEN 
				SET p_nombreDia = 'DOMINGO';
            ELSE
				SET p_nombreDia = null;
        END CASE;
END$$
DELIMITER ;
Ejemplo de uso:
CALL utilidades_getDia(3,@nombreDia);    -- Devuelve MIERCOLES
SELECT @nombreDia;

CALL utilidades_getDia(20,@nombreDia);   -- Devuelve NULL
SELECT @nombreDia;
NOTA: Veremos más adelante que este es ejemplo en que sería más adecuado el uso de una función o no de un procedimiento almacenado.



  • La sintaxis de CASE admite una variante cuando queremos tener un tipo de comparación que no sea 'igual', por ejemplo >= o <= o <>....
En ese caso, la cláusula CASE no lleva variable y es el las opciones WHEN donde se añade la variable junto con el tipo de comparación.
Aplicado a otro ejemplo donde se asigne 'SABADO','DOMINGO' o 'LABORABLE' en función del día:
CASE 
	  WHEN p_numDia>=1 AND p_numDia<=5 THEN 
  	        SET p_cadena = 'LABORABLE';
 	  WHEN p_numDia=6 THEN 
		SET p_cadena = 'SABADO';
	  WHEN p_numDia=7 THEN 
		SET p_cadena = 'DOMINGO';
END CASE;


  • Nota importante: Si al ejecutar el procedimiento os aparece el error: 1339. Case not found for CASE statement
Esto es debido a que en Mysql el CASE obliga a que siempre exista una opción por la que deba ir. Es decir, la opción ELSE siempre debe de estar si existen valores de la variable que estáis usando, que no entren por ningún WHEN.
En ese caso, podéis poner: ELSE BEGIN END;



  • Existe una función CASE que se emplea para control de flujo y que tiene una sintaxis un poco diferente.
Este tipo de función se puede emplear en el interior de una sentencia SELECT de la forma:
SELECT nombre, case anhos
 		 when 1 then 'Pequeño'
                 when 2 then 'Mediano'
                 else 'Grande'
	        end as anhos
FROM ANIMALES;



Instrucciones Repetitivas


  • Existen varias opciones para crear un bucle en el que un conjunto de instrucciones se repiten hasta o mientras se cumpla una determinada condición.
  • WHILE: Se repite mientras se cumpla la condición.
  • REPEAT: Se repite hasta que se cumpla la condición.
  • LOOP: Se repite el bucle hasta que se ejecute la sentencia LEAVE.


  • NOTA IMPORTANTE: Al trabajar con bucles, si no realizamos una programación correcta, podremos quedar en un bucle infinito.
Si eso sucede, desde el MySqlWorkBench podréis parar la ejecución presionando el botón indicado en la imagen siguiente:
Mod BD Prog Proc 14.jpg




REPEAT

Fijarse que el END REPEAT debe llevar punto y coma si a continuación le siguen nuevas instrucciones.
Mod BD Prog ControlFlujo 4.jpg
Imagen obtenida de https://dev.mysql.com


El uso es el mismo que en el caso de la programación estructurada. Repetimos un conjunto de instrucciones entre REPEAT y END REPEAT hasta que se cumpla la condición indicada en UNTIL.
Veamos un ejemplo de uso.
Crear un procedimiento de nombre utilidades_getSumEntreNumeros que sume los números entre un rango indicado por dos parámetros de entrada (incluidos ambos) y devuelva la suma en un parámetro de salida.
En el caso de que el primer parámetro tenga un valor superior al segundo, se debe devolver -1 y mostrar la cadena EL PRIMER NUMERO TIENE QUE SER INFERIOR AL SEGUNDO.
USE CIRCO;
DROP PROCEDURE IF EXISTS utilidades_getSumEntreNumeros;
DELIMITER $$
CREATE PROCEDURE utilidades_getSumEntreNumeros(p_numInferior int,p_numSuperior int, OUT p_sumaNumeros bigint)		
    COMMENT 'Suma los números entre un rango indicado por dos parámetros de entrada y devuelva la suma en un parámetro de salida.'
BEGIN
        SET p_sumaNumeros = 0;      -- En vez de declarar una variable local para guardar la suma podemos aprovechar el parámetro de salida.
                                    -- Hay que tener cuidado ya que el valor por defecto es null y por tanto no podesmos sumar sin inicializarlo

        IF (p_numSuperior < p_numInferior) THEN
            SET p_sumaNUmeros = -1;
                SELECT 'EL PRIMER NUMERO TIENE QUE SER INFERIOR AL SEGUNDO';
        ELSE
            REPEAT
                SET p_sumaNumeros = p_sumaNumeros + p_numInferior;
                SET p_numInferior = p_numInferior + 1;
            UNTIL p_numInferior > p_numSuperior
            END REPEAT;	
        END IF;

END$$
DELIMITER ;
Ejemplo de uso:
CALL utilidades_getSumEntreNumeros(3,50,@suma);
SELECT @suma;

CALL utilidades_getSumEntreNumeros(60,1,@suma);		-- Mostrará el mensaje de error
SELECT @suma;


  • Relacionado con este tipo de instrucción (y lo mismo es aplicable a WHILE y LOOP) podemos hacer uso de etiquetas para cuando queramos podamos salir del bucle (o empezar nuevamente) sin necesidad de esperar a que deje de cumplir la condición indicada en el UNTIL.
La idea es etiquetar el BEGIN - END que conforman el conjunto de instrucciones del bucle, y poder aplicar dos instrucciones:
  • ITERATE label: Deja de ejecutar las sentencias que estén a continuación y vuelve al comienzo del bucle. Es equivalente a 'continue' en la programación con Java.
  • LEAVE label: Sale del bucle que se encuentra etiquetado con el label indicado. Si no se pone label, saldría del procedimiento. Es equivalente a la sentencia 'break' en la programación con Java.


Es decir, defino un bucle con etiquetas de esta forma:
label_bucle: REPEAT


UNTIL condicion
END REPEAT label_bucle;
Ahora dentro del bucle puedo emplear las órdenes;
  • LEAVE label_bucle; -- Sale del bucle
  • ITERATE label_bucle; -- Vuelve al REPEAT y no continúa


Veamos un ejemplo. Es una modificación del ejemplo anterior.
Crear un procedimiento de nombre utilidades_getSumEntreNumerosPares que sume los números pares entre un rango indicado por dos parámetros de entrada (incluidos ambos) y devuelva la suma en un parámetro de salida. En caso de que la suma sea superior a 120000 debe devolver 120000 y salir del bucle.
Deberás considerar el caso de que se envíen los números un cualquier orden.
Para obtener el resto de dividir por 2 y saber si es par puedes hacer uso del operador % o MOD.
USE CIRCO;
DROP PROCEDURE IF EXISTS utilidades_getSumEntreNumerosPares;
DELIMITER $$
CREATE PROCEDURE utilidades_getSumEntreNumerosPares(p_numInferior int,p_numSuperior int, OUT p_sumaNumeros bigint)		
    COMMENT 'Suma los números pares entre un rango indicado por dos parámetros de entrada y devuelva la suma en un parámetro de salida.'
BEGIN
    DECLARE v_temporal int;

    SET p_sumaNumeros = 0;      -- En vez de declarar una variable local para guardar la suma podemos aprovechar el parámetro de salida.
                                -- Hay que tener cuidado ya que el valor por defecto es null y por tanto no podesmos sumar sin inicializarlo


    IF (p_numSuperior < p_numInferior) THEN	-- Intercambiamos los valores de los parámetros
        SET v_temporal = p_numSuperior;
        SET p_numSuperior = p_numInferior;
        SET p_numInferior = v_temporal;
    END IF;

    bucle_1: REPEAT
        IF (p_numInferior % 2 = 0) THEN
            SET p_sumaNumeros = p_sumaNumeros + p_numInferior;
        END IF;

        IF (p_sumaNumeros > 120000) THEN
            SET p_sumaNumeros = 120000;
            LEAVE bucle_1;
        END IF;

        SET p_numInferior = p_numInferior + 1;
    UNTIL p_numInferior > p_numSuperior
    END REPEAT bucle_1;	

END$$
DELIMITER ;
Ejemplo de uso:
CALL utilidades_getSumEntreNumerosPares(8,13000,@suma);  -- Se pasa. Da 120.000
SELECT @suma;

CALL utilidades_getSumEntreNumerosPares(13,8,@suma);  -- Da 30
SELECT @suma;



  • Nota: Se puede hacer uso de las etiquetas para salir del procedimiento entero, etiquetando el begin-end del procedimiento de la forma:
CREATE PROCEDURE sp_name()
sp: BEGIN
    IF condition THEN
        LEAVE sp;
    END IF;
    -- other statement
END$$



WHILE

Fijarse que el END WHILE debe llevar punto y coma si a continuación le siguen nuevas instrucciones.
Mod BD Prog ControlFlujo 5.jpg
Imagen obtenida de https://dev.mysql.com


El uso es el mismo que en el caso de la programación estructurada. Repetimos un conjunto de instrucciones MIENTRAS se cumpla la condición indicada en el WHILE.
Al igual que repeat, podemos hacer uso de etiquetas para emplear con las instrucciones LEAVE y ITERATE.


Veamos un ejemplo de uso.
Crea un procedimiento de nombre atracciones_getNumPorMes al que se le pase dos datos: un mes en letra (ENERO, FEBRERO,....) y un año en número. Tendrá que calcular la suma de todas las atracciones celebradas en el mes y año indicado y devolver el dato en un parámetro de salida. En el caso de que el año indicado sea null, tomar el año actual.
Crea un bucle que recorra todos los días del mes del año indicado y vaya sumando las atracciones.
Si el mes no existe debe devolver -1 en el parámetro de salida.
Para 'construír' la fecha emplear la función CONCAT. Debéis de tener en cuenta que hay meses en los que no hay 31 días. En vez de establecer condiciones por meses, emplear la función DATE (ya vista antes) para comprobar si la fecha es válida.
Nota:
  • Mysql no tiene la posibilidad de 'obligar' a que un parámetro admita valores NULL o NOT NULL y tendremos que gestionar nosotros la comprobación con IF y el envío de un SIGNAL (lo veremos en la gestión de errores) para provocar un error...En SQLServer sí se permite en la definición del parámetro establecer si permite valores nulos o no.
  • Fijarse que el código que vais a implementar para que en función del mes en letra devuelva un número es un candidato perfecto para una función, que veremos más adelante como implementarla.


USE CIRCO;
DROP PROCEDURE IF EXISTS atracciones_getNumPorMes;
DELIMITER $$
CREATE PROCEDURE atracciones_getNumPorMes(p_mes varchar(10),p_ano smallint, OUT p_numAtracciones int)		
    COMMENT 'Calcula la suma de todas las atracciones celebradas en el mes y año indicado y devolver el dato en un parámetro de salida'
label_proc: BEGIN
    DECLARE v_mesEnNumero tinyint default 0;
    DECLARE v_dia tinyint default 1;	-- Día del mes. Usado para hacer el bucle hasta el día 31
    DECLARE v_numAtraccPorDia int;

    SET p_numAtracciones = 0;		-- Por defecto vale NULL y no podríamos usarla para sumar las atracciones diarias
    
    CASE p_mes
        WHEN 'ENERO' THEN SET v_mesEnNumero = 1;
        WHEN 'FEBRERO' THEN	SET v_mesEnNumero = 2;
        WHEN 'MARZO' THEN SET v_mesEnNumero = 3;
        WHEN 'ABRIL' THEN SET v_mesEnNumero = 4;
        WHEN 'MAYO' THEN SET v_mesEnNumero = 5;
        WHEN 'JUNIO' THEN SET v_mesEnNumero = 6;
        WHEN 'JULIO' THEN SET v_mesEnNumero = 7;
        WHEN 'AGOSTO' THEN SET v_mesEnNumero = 8;
        WHEN 'SEPTIEMBRE' THEN SET v_mesEnNumero = 9;
        WHEN 'OCTUBRE' THEN SET v_mesEnNumero = 10;
        WHEN 'NOVIEMBRE' THEN SET v_mesEnNumero = 11;
        WHEN 'DICIEMBRE' THEN SET v_mesEnNumero = 12;
        ELSE 
            SET p_numAtracciones = -1;
            LEAVE label_proc;       -- Sale del procedimiento
    END CASE;
    
    IF (p_ano IS NULL) THEN
        SET p_ano = YEAR(curdate());
    END IF;
    
    bucle_dia: WHILE (v_dia <= 31) DO
        IF (DATE(CONCAT(p_ano,'-',v_mesEnNumero,'-',v_dia)) IS NULL) THEN	-- No es una fecha válida por meses que pueden tener menos de 31 dias
            LEAVE bucle_dia;
        END IF;

        SELECT COUNT(*)
        INTO v_numAtraccPorDia
        FROM ATRACCION_DIA
        WHERE fecha = CONCAT(p_ano,'-',v_mesEnNumero,'-',v_dia);
        
        SET v_dia = v_dia+1;
        SET p_numAtracciones = p_numAtracciones + v_numAtraccPorDia;
    END WHILE bucle_dia;

END $$
DELIMITER ;
Ejemplo de uso:
CALL atracciones_getNumPorMes('ABRIL',2000,@numAtracciones);   -- Devuelve 2.  Podéis añadir nuevas entradas a la tabla para comprobar el funcionamiento.
SELECT @numAtracciones;

CALL atracciones_getNumPorMes('NO EXISTE',2000,@numAtracciones);   -- Devuelve -1. 
SELECT @numAtracciones;


CALL atracciones_getNumPorMes('ABRIL',NULL,@numAtracciones);   -- Buscar en el año actual
SELECT @numAtracciones;



LOOP

Fijarse que el END LOOP debe llevar punto y coma si a continuación le siguen nuevas instrucciones.
Mod BD Prog ControlFlujo 6.jpg
Imagen obtenida de https://dev.mysql.com


El uso es el mismo que en el caso de la programación estructurada. Repetimos un conjunto de instrucciones (vendría ser a ser equivalente a WHILE(true).
En este tipo de bucle es necesario emplear la orden LEAVE para poder salir del mismo.
Al igual que repeat, podemos hacer uso de etiquetas para emplear con las instrucciones LEAVE y ITERATE.


Veamos un ejemplo de uso.
Crea un procedimiento de nombre utilidades_cambiarFormato al que se le pase un dato en forma de cadena y devuelva en el mismo parámetro la cadena enviada cambiando las letras por números y separados por un guion.
Para pasar de letra a número emplea la función ORD.
Para recorrer la cadena carácter a carácter puedes hacer uso de las funciones LEFT y RIGHT o de la función SUBSTRING (más fácil) además de la función CHAR_LENGTH.
Tener cuidado ya que la función SUBSTRING, el parámetro pos, la primera posición de la cadena, empieza en 1, no en 0.



USE CIRCO;
DROP PROCEDURE IF EXISTS utilidades_cambiarFormato;
DELIMITER $$
CREATE PROCEDURE utilidades_cambiarFormato(INOUT p_cadena varchar(1000))		
    COMMENT 'Devuelve en el mismo parámetro la cadena enviada cambiando las letras por números y separados por un guion'
BEGIN
    DECLARE v_indice smallint default 1;   -- Empleado para recorrer la cadena caracter a caracter
    DECLARE v_caracter char(1);            -- Guarda el caracter de la cadena
    
    DECLARE v_cadena varchar(1000) default '';        -- Cadena a devolver. Debemos poner un default para poder concatenar
    
    bucle_loop: LOOP
        SET v_caracter = SUBSTRING(p_cadena,v_indice,1);
        SET v_cadena = CONCAT(v_cadena,ORD(v_caracter),'-');	

        SET v_indice = v_indice+1;
        IF (v_indice > CHAR_LENGTH(p_cadena)) THEN   
            LEAVE bucle_loop;
        END IF;
    END LOOP bucle_loop;
    
    SET p_cadena = v_cadena;

END $$
DELIMITER ;


Ejemplo de uso:
SET @dato = 'ANGEL';
CALL utilidades_cambiarFormato(@dato);
SELECT @dato;



Ejercicios propuestos instrucciones repetitivas

Nota: Muchas veces el mismo ejercicio puede solucionarse empleando cualquiera de las tres instrucciones repetitivas. Para practicar intenta hacer 3 versiones por cada ejercicio empleando cada una de los tipos vistos anteriormente.



  • Ejercicio 1: Crea un procedimiento de nombre pistas_getSumAforo que devuelva en forma de parámetro de salida la suma de los aforos de las pistas enviadas como parámetro. El formato de la lista de pistas es una única cadena: 'pista1,pista2,pista3,....'
Deberás, empleando la función SUBSTRING y LOCATE, descomponer el dato en cada una de las pistas y buscar el aforo de cada una de ellas, devolviendo la suma de los aforos de todas.
En case de que alguna pista no exista, deberás de mostrar la cadena 'La pista ZZZZZZ no existe' por cada pista que no exista.
Debemos de controlar que al menos siempre se envíe una pista. En caso contrario debe de enviarse -1 en el parámetro de salida (emplea la función CHAR_LENGTH).


  • Ejercicio 2: Crea un procedimiento de nombre atracciones_getGananciasSupuestas en el que se le pase como datos el nombre de una atracción y 2 números. Deberá de mostrar los días en los que se celebró la atracción junto con su número de espectadores y ganancia así como las ganancias que tendríamos si la entrada costara diferentes precios. Los precios deben variar desde el primer número enviado hasta el segundo, con una diferencia de 5 euros entre ellos.

Es decir, imaginemos que la atracción1 se celebró estos días:

pista, fecha,numEspect,Ganancia
  • pista1,01/01/2000,100,1000
  • pista1,02/01/2000,120,1200

Si envío al procedimiento estos datos: 'pista1',20,30 deberá mostrar que ganancia tenemos con un precio de entrada de 20,25 y 30 de la forma:

pista, fecha,numEspect,Ganancia, Ganancia con 20 euros=20xNumEspect (dará 3 filas)
pista, fecha,numEspect,Ganancia, Ganancia con 25 euros=25xNumEspect (dará 3 filas)
pista, fecha,numEspect,Ganancia, Ganancia con 30 euros=30xNumEspect (dará 3 filas)
Debes de controlar que los números enviados sean múltiplos de 5 y que el segundo sea mayor que el primero. Si no se cumplen las condiciones, debe mostrar un texto aclarando el error.
En caso de que la atracción no exista o que la atracción exista pero no ha tenido representaciones, también debe informar del error.



  • Ejercicio 3: Crea un procedimiento de nombre utilidades_getNumImpares al que se le pasen tres números y devuelva en forma de parámetro de salida cuanto números hay entre los dos primeros números que son divisibles por el tercero. Se deben incluir los dos primeros números a la hora de contar.
No importa el orden en el que se envíen los dos primeros números.
Por ejemplo: 5,1,2 => Busca los números múltiplos de 2 entre 1 y 5 (inclúidos) => Mostrará: 2






Solución Ejercicios propuestos instrucciones repetitivas
  • Ejercicio 1: Crea un procedimiento de nombre pistas_getSumAforo que devuelva en forma de parámetro de salida la suma de los aforos de las pistas enviadas como parámetro. El formato de la lista de pistas es una única cadena: 'pista1,pista2,pista3,....'
Deberás, empleando la función SUBSTRING y CHAR_LENGTH, descomponer el dato en cada una de las pistas y buscar el aforo de cada una de ellas, devolviendo la suma de los aforos de todas.
En de que alguna pista no exista, deberás de mostrar la cadena 'La pista ZZZZZZ no existe' por cada pista que no exista.
Debemos de controlar que al menos siempre se envíe una pista. En caso contrario debe de enviarse -1 en el parámetro de salida.
USE CIRCO;
DROP PROCEDURE IF EXISTS animales_getSumAforo;
DELIMITER $$
CREATE PROCEDURE animales_getSumAforo (p_pistas varchar(100),OUT p_sumaAforo int)		
    COMMENT 'Devuelva en forma de parámetro de salida la suma de los aforos de las pistas enviadas como parámetro con formato pista1,pista2,...'
label_getSumAforo: BEGIN
    DECLARE v_index,v_pos smallint default 1;
    DECLARE v_aforo smallint;		-- El tipo coincide con el declarado en la tabla
    DECLARE v_nombrePista varchar(50);	-- El tipo coincide con el declarado en la tabla

    SET p_sumaAforo = 0;		-- Por defecto vale null. Como vamos ir acumulando el aforo en esta variable no podemos sumar un valor null. Por eso le damos un valor inicial
    
    IF (CHAR_LENGTH(p_pistas)=0) THEN
        SET p_sumaAforo = -1;
        LEAVE label_getSumAforo;
    END IF;
    
    WHILE (v_pos <> 0) DO   -- La función LOCATE devuelve 0 si no encuentra la cadena a buscar (la coma que separa cada pista).
        SELECT LOCATE(',',p_pistas,v_index)
        INTO v_pos;
        IF (v_pos <> 0) THEN	-- Encuentra una coma
            SELECT SUBSTRING(p_pistas,v_index,v_pos-v_index)
            INTO v_nombrePista;
            
            set v_index = v_pos+1;
        ELSE    -- Falta la última pista con contar ya que no lleva coma al final
            SELECT SUBSTRING(p_pistas,v_index)
            INTO v_nombrePista;
        END IF;
        
        SET v_aforo = null; -- Para comprobar si la pista existe, podríamos hacer un count(*) buscando por la pista
                            -- Pero de esta forma nos ahorramos una consulta. Si la pista no existe el valor de la variable seguirá valiendo NULL
                            -- En SQL Server podemos hacer uso de la variable @@ROWCOUNT
        SELECT aforo
        INTO v_aforo
        FROM PISTAS
        WHERE nombre = v_nombrePista;
        
        IF (v_aforo IS NULL) THEN 	-- Pista no encontrada
            SELECT CONCAT('La pista ',v_nombrePista,' no existe');
        ELSE
            SET p_sumaAforo = p_sumaAforo + v_aforo;
        END IF;
        
    END WHILE;
    
END $$
DELIMITER ;

Ejemplo de uso:

CALL animales_getSumAforo('LATERAL1,LATERAL2,NO EXISTE',@aforo);  -- Una de las pistas no existe
SELECT @aforo;

CALL animales_getSumAforo('SUPER',@aforo);   -- Devuelve el aforo de SUPER
SELECT @aforo;

CALL animales_getSumAforo('',@aforo);   -- Devuelve -1
SELECT @aforo;



  • Ejercicio 2: Crea un procedimiento de nombre atracciones_getGananciasSupuestas en el que se le pase como datos el nombre de una atracción y 2 números. Deberá de mostrar los días en los que se celebró la atracción junto con su número de espectadores y ganancia así como las ganancias que tendríamos si la entrada costara diferentes precios. Los precios deben variar desde el primer número enviado hasta el segundo, con una diferencia de 5 euros entre ellos. Es decir, si envío los datos 'pista1',20,30 deberá mostrar que ganancia tenemos con un precio de entrada de 20,25 y 30 (se debe mostrar cada precio en una nueva consulta, es decir, una consulta mostrará 'atraccion1', 100, 3000 (estos serían los datos actuales), 20x100 (con un precio de 20 euros por entrada), la siguiente consulta debe mostrar lo mismo pero con 25x100 y la tercera consulta debe mostrar lo mismo pero 30x100).
Debes controlar que los números enviados sean múltiplo de 5 y que el segundo es mayor que el primero. Si no se cumplen las condiciones, debe mostrar un texto aclarando el error.
En caso de que la atracción no exista o que la atracción existe pero no ha tenido representaciones, también debe informar del error.
USE CIRCO;
DROP PROCEDURE IF EXISTS atracciones_getGananciasSupuestas;
DELIMITER $$
CREATE PROCEDURE atracciones_getGananciasSupuestas (p_nombreAtraccion varchar(50),p_precioInferior tinyint, p_precioSuperior tinyint)		
    COMMENT 'Muestra los días en los que se celebró la atracción junto con su número de espectadores y ganancia así como las ganancias que tendríamos si la entrada costara diferentes precios'
label_getGananciasSupuestas: BEGIN
    DECLARE v_precio tinyint default 0;
    DECLARE v_atraccionExiste tinyint;	-- Para comprobar si la atraccion existe

    SELECT COUNT(*)			-- Comprobamos si la atraccion existe
    INTO v_atraccionExiste
    FROM ATRACCIONES
    WHERE nombre = p_nombreAtraccion;
    IF (v_atraccionExiste=0) THEN
        SELECT 'La atracción no existe';
        LEAVE label_getGananciasSupuestas; 
    END IF;

    SELECT COUNT(*)			-- Comprobamos si tiene alguna atraccion
    INTO v_atraccionExiste
    FROM ATRACCION_DIA
    WHERE nombre_atraccion = p_nombreAtraccion;
    IF (v_atraccionExiste=0) THEN
        SELECT 'La atracción no ha celebrado ninguna actuación';
        LEAVE label_getGananciasSupuestas; 
    END IF;

    IF (p_precioInferior > p_precioSuperior) THEN
        SELECT 'El precio inferior no puede ser superior al segundo número';
        LEAVE label_getGananciasSupuestas;
    END IF;
    IF ((p_precioInferior % 5)<>0 OR (p_precioSuperior%5)<>0) THEN
        SELECT 'Los dos números deben ser múltiplos de 5';
        LEAVE label_getGananciasSupuestas;
    END IF;
    
    SET v_precio = p_precioInferior;
    REPEAT
        SELECT nombre_atraccion, num_espectadores, ganancias, num_espectadores*v_precio as gananciaSimulada
        FROM ATRACCION_DIA
        WHERE nombre_atraccion = p_nombreAtraccion;
        
        SET v_precio = v_precio + 5;    
    UNTIL v_precio > p_precioSuperior
    END REPEAT;
END $$
DELIMITER ;

Ejemplo de uso:

CALL atracciones_getGananciasSupuestas('El orangután',12,20);  -- Muestra mensaje de error ya que los precios no son múltiplos de 5


CALL atracciones_getGananciasSupuestas('El orangután',10,20);  -- Muestra mensaje de error ya que esa atracción no se ha celebrado nunca.


CALL atracciones_getGananciasSupuestas('El gran felino',10,20);  -- Muestra tres pestañas cada una con todas las celebraciones y ganancias simuladas



  • Ejercicio 3: Crea un procedimiento de nombre utilidades_getNumImpares al que se le pasen tres números y devuelva en forma de parámetro de salida cuanto números hay entre los dos primeros números que son divisibles por el tercero. Se deben incluir los dos primeros números a la hora de contar.
No importa el orden en el que se envíen los dos primeros números.
Por ejemplo: 5,1,2 => Busca los números múltiplos de 2 entre 1 y 5 (inclúidos) => Mostrará: 2


USE CIRCO;
DROP PROCEDURE IF EXISTS utilidades_getNumImpares;
DELIMITER $$
CREATE PROCEDURE utilidades_getNumImpares (p_numero1 int, p_numero2 int,p_multiplo int, OUT p_numMultiplos int)		
    COMMENT 'Muestra cuanto números múltiplos por el tercer número hay entre los dos primeros (incluidos los dos números enviados)'
BEGIN
    DECLARE v_temporal int default 0;
    
    IF (p_numero1 > p_numero2) THEN		-- Intercambiamos los números si el segundo es inferior al primero
        SET v_temporal = p_numero2;
        SET p_numero2 = p_numero1;
        SET p_numero1 = v_temporal;
    END IF;
    
    SET p_numMultiplos = 0;
    
    etiqueta_bucle: LOOP	-- Probamos con LOOP. Valdría WHILE o REPEAT
        IF (p_numero1%p_multiplo=0) THEN 		-- Es un número IMPAR
            SET p_numMultiplos = p_numMultiplos + 1;
        END IF;
        SET p_numero1 = p_numero1 + 1;
        IF (p_numero1 > p_numero2) THEN
            LEAVE etiqueta_bucle;
        END IF;
    END LOOP;
END $$
DELIMITER ;

Ejemplo de uso:

CALL utilidades_getNumImpares(5,1,3,@numMultiplos);  -- Busca múltiplos de 3
SELECT @numMultiplos;

CALL utilidades_getNumImpares(25,2,2,@numMultiplos);   -- Busca múltiplos de 2
SELECT @numMultiplos;




Enlace a la página principal de la UD7

Enlace a la página principal del curso




-- Ángel D. Fernández González -- (2020).