Está en la página 1de 11

Hace mucho tiempo que quería escribir sobre esto.

Y aunque es recomendable no
abusar de los bucles en MySQL, es más, a veces no son necesarios, y siempre hay
que buscar una solución que no los use, a veces no la vamos a encontrar y será en
esos casos cuando debamos utilizarlos.

Veamos un bucle muy sencillo, parecido a un for de los de toda la vida, en el que
contamos del 1 al 9:

1 DELIMITER $$
2 CREATE PROCEDURE simple_loop ( )
3 BEGIN
4   DECLARE counter BIGINT DEFAULT 0
5;

7   my_loop: LOOP
8     SET counter=counter+1;
9
10     IF counter=10 THEN
11       LEAVE my_loop;
12     END IF;
13
14     SELECT counter;
15
16   END LOOP my_loop;
END$$
17
DELIMITER ;

cuando hagamos:

1 CALL simple_loop();

Cual es el resultado

+———+
| counter |
+———+
| 1 |
+———+
1 row in set (0.01 sec)

+———+
| counter |
+———+
| 2 |
+———+
1 row in set (0.01 sec)

+———+
| counter |
+———+
| 3 |
+———+
1 row in set (0.01 sec)

+———+
| counter |
+———+
| 4 |
+———+
1 row in set (0.01 sec)

+———+
| counter |
+———+
| 5 |
+———+
1 row in set (0.01 sec)+———+
| counter |
+———+
| 6 |
+———+
1 row in set (0.01 sec)

+———+
| counter |
+———+
| 7 |
+———+
1 row in set (0.01 sec)

+———+
| counter |
+———+
| 8 |
+———+
1 row in set (0.01 sec)

+———+
| counter |
+———+
| 9 |
+———+
1 row in set (0.01 sec)

Query OK, 0 rows affected (0.01 sec)

Vemos que el código que iteraremos está entre LOOP…END LOOP, lo que aparece
justo antes (my_loop) es una etiqueta para nombrar ese bucle. Ahora bien, en este
ejemplo simplemente incrementamos la variable counter, y con una condición IF
hacemos que el bucle llegue a su fin cuando counter sea 10. Ese 10 no lo veremos
porque abandonamos el bucle antes del SELECT.

Propongamos un ejemplo más complicado, vamos a registrar las puntuaciones


obtenidas en un juego, este juego consistirá en una prueba que debemos realizar en
el menor tiempo posible, a pata coja y con obstáculos, y tenemos dos tipos de falta,
uno es apoyar la pierna levantada, y otra es chocar con un obstáculo, al final de la
prueba se asignarán los puntos y se almacenarán en la tabla, para no tener que
calcularlos cada vez.

1 CREATE TABLE Runners (
   
2 Runner_id BIGINT NOT NULL AUTO_INCREMENT,
    Name VARCHAR(120) NOT NULL,
3     Time BIGINT NOT NULL,
    Penalty1 BIGINT NOT NULL,
4     Penalty2 BIGINT NOT NULL,
5

6
    Points BIGINT,
    PRIMARY KEY (Runner_id)
7
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;

Ahora introducimos algo de información para probar:

2 INSERT INTO Runners VALUES (NULL, 'Michael', 123, 5, 2, NULL)
;
3 INSERT INTO Runners VALUES (NULL, 'Sarah', 83, 3, 3, NULL);
INSERT INTO Runners VALUES (NULL, 'John', 323, 1, 1, NULL);
4 INSERT INTO Runners VALUES (NULL, 'Ramon', 100, 8, 4, NULL);
INSERT INTO Runners VALUES (NULL, 'Andrew', 143, 4, 3, NULL);
5 INSERT INTO Runners VALUES (NULL, 'Antoine', 199, 3, 2, NULL)
;
6 INSERT INTO Runners VALUES (NULL, 'David', 101, 2, 1, NULL);

Lo primero que vamos a hacer, será un procedimiento que incluya el bucle básico, con
SELECTs, para ver que todo funciona y que lo estamos haciendo bien. (Debajo
explicaré para qué es cada cosa):

1 DROP PROCEDURE IF EXISTS cursorTest;
2 DELIMITER $$
3 CREATE PROCEDURE cursorTest (
4 ) BEGIN
5 -- Variables donde almacenar lo que nos traemos desde el SELECT
6   DECLARE v_name VARCHAR(120);
7   DECLARE v_time BIGINT;
  DECLARE v_penalty1 BIGINT;
8
  DECLARE v_penalty2 BIGINT;
9
-- Variable para controlar el fin del bucle
10
  DECLARE fin INTEGER DEFAULT 0;
11
12
-- El SELECT que vamos a ejecutar
13
  DECLARE runners_cursor CURSOR FOR
14
    SELECT Name, Time, Penalty1, Penalty2 FROM Runners;
15
16
-- Condición de salida
17
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET fin=1;
18
19
  OPEN runners_cursor;
20
  get_runners: LOOP
21
    FETCH
22
runners_cursor INTO v_name, v_time, v_penalty1, v_penalty2;
23
    IF fin = 1 THEN
24
       LEAVE get_runners;
25
    END IF;
26
27
  SELECT v_name, v_time, v_penalty1, v_penalty2;
28
29
  END LOOP get_runners;
30
31
  CLOSE runners_cursor;
32
END$$
33
DELIMITER ;

Tenemos que tener en cuenta que en este cursor recorreremos el resultado de un


SELECT y en cada fila podremos almacenar el valor de cada campo en variables (por
eso declaramos v_name, v_time, v_penalty y v_penalty1). Al final, cada fila será como
un SELECT Name, Time, Penalty1, Penalty2 INTO v_name, v_time, v_penalty1,
v_penalty2 WHERE … y en cada iteración, tendremos unos valores para esas
variables, correspondiendo con filas obtenidas de forma consecutiva. Para esto es el
DECLARE xxx CURSOR FOR SELECT …

Tenemos que poner también una condición de finalización, normalmente, cuando no


haya más filas, por eso el DECLARE CONTINUE HANDLER FOR NOT FOUND SET
fin=1, en ese caso, cuando no encontremos más filas, pondremos un 1 en la variable
fin.
Dentro del bucle, analizaremos el valor de la variable fin para ver si finalizamos
(LEAVE xxxxx) o ejecutamos una iteración.

Demos un paso más, vamos a crear una función que asigne las puntuaciones a cada
uno de los corredores con una fórmula. Por ejemplo la siguiente: siendo Time el
tiempo en segundos que se tarda en realizar la prueba, 500-Time serán los puntos
iniciales, a los que tenemos que restar 5*penalty1 y 3*penalty2. Por tanto:

DROP FUNCTION IF EXISTS calculate_runner_points;
1
DELIMITER $$
2
CREATE FUNCTION calculate_runner_points (
3
  In_time BIGINT,
4
  In_penalty1 BIGINT,
5
  In_penalty2 BIGINT
6
) RETURNS BIGINT
7
BEGIN
8
  DECLARE points BIGINT;
9
 
10
  SET points = 500 - In_time - In_penalty1*5 - In_penalty2*3
11
;
12
 
13
  RETURN points;
14
END$$
15
DELIMITER ;

Ahora el código para calcular los puntos de los jugadores puede ser:

1 DROP PROCEDURE IF EXISTS calculate_all_points;
2 DELIMITER $$
3 CREATE PROCEDURE calculate_all_points (
4 ) BEGIN
5 -- Variables donde almacenar lo que nos traemos desde el SELECT
6   DECLARE v_name VARCHAR(120);
7   DECLARE v_time BIGINT;
8   DECLARE v_penalty1 BIGINT;
9   DECLARE v_penalty2 BIGINT;
1   DECLARE v_runner_id BIGINT;
0 -- Variable para controlar el fin del bucle
1   DECLARE fin INTEGER DEFAULT 0;
1
1 -- El SELECT que vamos a ejecutar
2   DECLARE runners_cursor CURSOR FOR
1     SELECT Runner_id, Name, Time, Penalty1, Penalty2 FROM Runners;
3
1 -- Condición de salida
4   DECLARE CONTINUE HANDLER FOR NOT FOUND SET fin=1;
1
5   OPEN runners_cursor;
1   get_runners: LOOP
6     FETCH runners_cursor INTO v_runner_id, v_name, v_time, v_penalty1,
1 v_penalty2;
7     IF fin = 1 THEN
1        LEAVE get_runners;
8     END IF;
1
9   UPDATE Runners SET Points=calculate_runner_points(v_time, v_penalty1
2,
0 v_penalty2) WHERE Runner_id=v_runner_id;
2
1   END LOOP get_runners;
2
2   CLOSE runners_cursor;
2 END$$
3 DELIMITER ;
2
4
2
5
2
6
2
7
2
8
2
9
3
0
3
1
3
2
3
3
3
4
3
5
3
6

Pero claro, como dije al principio, tenemos que mirar siempre, si hay alguna
solución posible que no utilice bucles, sobre todo porque cuando empezamos a
utilizarlos nos emocionamos (igual que dice que también nos emocionamos con
las expresiones regulares) y vemos la solución con bucles más inmediata que
sin bucles, pero claro, con bucles todo irá mucho más lento. Podríamos haber
hecho:

UPDATE Runners SET Points=calculate_runner_points(Time, Penalty1, Penal
1
ty2);

Aunque podemos hacer algunas cosas más con el ejemplo del bucle, por ejemplo, si
el tiempo es mayor de 250, se intercambien los penalties, editando directamente el
código del bucle, metiendo una sentencia IF, aunque eso mismo lo podemos hacer
también desde la función que calcula los puntos.

Otro pequeño ejemplo (bueno, no tan pequeño) que me viene a la cabeza es que
tenemos un sistema de usuarios en el que cada usuario tiene información en tres
tablas: una para login, password e información de acceso; otra para información de
perfil y otra de permisos. En este caso, en todas las tablas excepto en la de permisos
habrá una entrada por usuario, pero en los permisos estableceremos el elemento
sobre el que un usuario tiene permiso y qué tipo de permiso tiene, y como podemos
tener permiso sobre varios objetos, puede haber varias entradas por usuario.
También tenemos una tabla de mensajes entre usuarios.
Por otro lado tenemos las páginas, que serán objetos de nuestro sistema y serán
sobre las que los usuarios podrán ver, editar, crear derivados y borrar (los diferentes
permisos del sistema), eso sí, para las páginas existe una jerarquía, por lo que
podremos tener páginas «hijas». Pero cuando creamos una página en el sistema:
 Al menos tendrán que tener permiso total sobre ella los administradores del
sistema (marcados en la tabla de acceso)
 Si un usuario tenía permiso de edición sobre una página padre, podrá editar la
nueva página hija
 Si un usuario podía crear derivadas en la página padre, podrá hacerlo en la hija
 Si un usuario podía editar y crear derivadas en la padre, podrá borrar en la hija
 Además, tenemos que enviar un mensaje (meter el mensaje en la tabla), al
usuario con los permisos que tendrá en la nueva página
 Tenemos para ello las funciones y procedimientos:
o puede_crear_derivadas(usuario, pagina) – Que devolverá TRUE si el
usuario puede crear páginas derivadas
o puede_editar(usuario, pagina) – Que hará lo mismo que la anterior pero
con el nuevo permiso
o nuevo_permiso(usuario, pagina, permiso) – Insertará un nuevo permiso
en la tabla de permisos
o mensaje(from, to, mensaje) – Enviará un usuario a un usuario.

Las funciones puede_crear_derivadas() y puede_editar() en principio son fáciles de


entender, pero su funcionamiento interno es mucho más complejo, las ha hecho un
compañero de trabajo y no tenemos ganas de meternos a ver qué ha liado. Lo mismo
pasa con nuevo_permiso() (que puede insertar entradas en la tabla o modificar las
existentes) o con mensaje(), que enviará notificaciones y además creará una tarea
para mandar el mensaje por e-mail, por lo que nuestro procedimiento para crear una
página quedaría así:

1 DROP PROCEDURE IF EXISTS crear_pagina;
2 DELIMITER $$
3 CREATE PROCEDURE crear_pagina (
4   IN in_nombre VARCHAR(120),
5   IN in_parent BIGINT
6 ) BEGIN
7 -- Variables donde almacenar lo que nos traemos desde el SELECT
8   DECLARE v_user_id BIGINT;
9   DECLARE v_crear_derivadas TINYINT;
10   DECLARE v_object_id BIGINT;
11   DECLARE v_mens TEXT;
12
13 -- Variable para controlar el fin del bucle
14   DECLARE fin INTEGER DEFAULT 0;
15
16 -- El SELECT que vamos a ejecutar
17   DECLARE users_cursor CURSOR FOR
18     SELECT User_id FROM Users;
19
20 -- Condición de salida
21   DECLARE CONTINUE HANDLER FOR NOT FOUND SET fin=1;
22
23   INSERT INTO Paginas (Nombre, Parent) VALUES (in_nombre, in_parent);
24   SELECT LAST_INSERT_ID() INTO v_object_id;
25
26   OPEN users_cursor;
27   get_users: LOOP
28     FETCH users_cursor INTO v_user_id;
29
30     IF fin = 1 THEN
31        LEAVE get_users;
32     END IF;
33
34     SET v_mens = CONCAT('Nuevos permisos sobre la pagina:
35 ',in_nombre,': ');  
36
37     IF puede_crear_derivadas(v_user_id, in_parent) THEN
38       CALL nuevo_permiso(v_user_id, v_object_id, 'derivadas');
39       SET v_mens = CONCAT(v_mens, 'Crear derivadas ');
40       SET v_crear_derivadas=1;
41     ELSE
42       SET v_crear_derivadas=0;
43     END IF;
44
45     IF puede_editar(v_user_id, in_parent) THEN
46       CALL nuevo_permiso(v_user_id, v_object_id, 'editar');
47       SET v_mens = CONCAT(v_mens, 'Editar ');
48       IF v_crear_derivadas=1 THEN
49          CALL nuevo_permiso(v_user_id, v_object_id, 'borrar');
50          SET v_mens = CONCAT(v_mens, 'Borrar ');
51       END IF;
52     END IF;
53
54     CALL mensaje(1, v_user_id, v_mens);
  END LOOP get_users;
55
56
  CLOSE users_cursor;
57
END$$
58
DELIMITER ;

Seguro que en el futuro se me ocurren ejemplos algo mejores, es más, se aceptan


sugerencias en los comentarios, intentaré recrear los ejemplos y resolverlos en futuros
posts.

También podría gustarte