REPEAT
El bucle REPEAT ejecuta un bloque de sentencias y luego verifica la condición de salida. La condición se evalúa después de cada iteración con UNTIL, lo que garantiza que el cuerpo se ejecute al menos una vez.
Si conoces el bucle do...while de C, Java o JavaScript, REPEAT es su equivalente en MySQL. La diferencia clave con WHILE es cuándo se evalúa la condición: WHILE la comprueba antes (y puede no ejecutarse nunca), REPEAT la comprueba después (y siempre se ejecuta al menos una vez).
REPEAT es la opción natural cuando sabes que necesitas ejecutar el cuerpo al menos una vez antes de decidir si continuar: "pide datos al usuario y repite hasta que sean válidos", "lee una fila del cursor y repite hasta que no haya más", "ejecuta el proceso y repite hasta que se cumpla la condición de parada".
Sintaxis
[etiqueta:] REPEAT
sentencias;
UNTIL condicion
END REPEAT [etiqueta];Un detalle sintáctico importante: la condición después de UNTIL no lleva punto y coma. Esto es una peculiaridad de MySQL que genera errores de sintaxis frecuentes en principiantes. Escribe UNTIL i > limite sin ; al final.
La condición en UNTIL expresa cuándo dejar de repetir (no cuándo continuar, como en WHILE). Es decir, el bucle continúa mientras la condición sea falsa y termina cuando es verdadera. Esto es opuesto a WHILE, donde la condición expresa cuándo continuar.
Ejemplo básico
DELIMITER //
CREATE PROCEDURE contar_repeat(IN limite INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE resultado VARCHAR(500) DEFAULT '';
REPEAT
SET resultado = CONCAT(resultado, i, ' ');
SET i = i + 1;
UNTIL i > limite
END REPEAT;
SELECT TRIM(resultado) AS numeros;
END //
DELIMITER ;CALL contar_repeat(5);| numeros |
|---|
| 1 2 3 4 5 |
El cuerpo se ejecuta, luego se evalúa i > limite. Si es falso (todavía no hemos pasado el límite), se repite. Si es verdadero (ya pasamos el límite), el bucle termina.
Se ejecuta al menos una vez
Esta es la característica definitoria de REPEAT y la razón principal para elegirlo sobre WHILE:
DELIMITER //
CREATE PROCEDURE repeat_una_vez()
BEGIN
DECLARE i INT DEFAULT 10;
DECLARE ejecuto VARCHAR(5) DEFAULT 'No';
REPEAT
SET ejecuto = 'Sí';
SET i = i + 1;
UNTIL i >= 5 -- Condición verdadera desde el inicio
END REPEAT;
SELECT ejecuto AS se_ejecuto, i AS valor_i;
END //
DELIMITER ;CALL repeat_una_vez();| se_ejecuto | valor_i |
|---|---|
| Sí | 11 |
Aunque i >= 5 era verdadero desde el inicio (i empezó en 10), el cuerpo se ejecutó una vez antes de evaluar la condición. Por eso ejecuto vale 'Sí' e i vale 11. Con WHILE, el cuerpo no se habría ejecutado nunca.
Esta propiedad puede ser tanto una ventaja como una fuente de bugs. Es una ventaja cuando necesitas que la primera iteración ocurra siempre (por ejemplo, hacer al menos un FETCH de un cursor). Es un riesgo si la primera ejecución puede causar efectos secundarios no deseados cuando la condición debería haber impedido cualquier ejecución.
Generar tabla de multiplicar
REPEAT es natural para generaciones de datos donde sabes que siempre habrá al menos un resultado:
DELIMITER //
CREATE PROCEDURE tabla_multiplicar(IN numero INT)
BEGIN
DECLARE i INT DEFAULT 1;
DROP TEMPORARY TABLE IF EXISTS tmp_tabla;
CREATE TEMPORARY TABLE tmp_tabla (
operacion VARCHAR(20),
resultado INT
);
REPEAT
INSERT INTO tmp_tabla VALUES (
CONCAT(numero, ' x ', i),
numero * i
);
SET i = i + 1;
UNTIL i > 10
END REPEAT;
SELECT * FROM tmp_tabla;
DROP TEMPORARY TABLE tmp_tabla;
END //
DELIMITER ;CALL tabla_multiplicar(7);| operacion | resultado |
|---|---|
| 7 x 1 | 7 |
| 7 x 2 | 14 |
| 7 x 3 | 21 |
| 7 x 4 | 28 |
| 7 x 5 | 35 |
| 7 x 6 | 42 |
| 7 x 7 | 49 |
| 7 x 8 | 56 |
| 7 x 9 | 63 |
| 7 x 10 | 70 |
Una tabla de multiplicar siempre tiene al menos una fila (1 x n), así que REPEAT encaja perfectamente: no necesitamos comprobar nada antes de la primera iteración.
REPEAT con cursor
REPEAT también funciona bien con cursores. En este caso, el primer FETCH se hace dentro del bucle (a diferencia del patrón con WHILE, donde el primer FETCH se hace antes del bucle):
DELIMITER //
CREATE PROCEDURE listar_productos_baratos(IN precio_max DECIMAL(10,2))
BEGIN
DECLARE v_nombre VARCHAR(100);
DECLARE v_precio DECIMAL(10,2);
DECLARE v_done INT DEFAULT FALSE;
DECLARE v_lista VARCHAR(2000) DEFAULT '';
DECLARE cur CURSOR FOR
SELECT nombre, precio FROM productos
WHERE precio <= precio_max AND activo = TRUE
ORDER BY precio;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
OPEN cur;
REPEAT
FETCH cur INTO v_nombre, v_precio;
IF NOT v_done THEN
SET v_lista = CONCAT(v_lista, v_nombre, ' (', v_precio, '), ');
END IF;
UNTIL v_done
END REPEAT;
CLOSE cur;
SELECT TRIM(TRAILING ', ' FROM v_lista) AS productos;
END //
DELIMITER ;CALL listar_productos_baratos(50);Observa que dentro del REPEAT necesitamos un IF para evitar procesar la fila después de que v_done sea TRUE (porque el FETCH que activa v_done ya se ejecutó como parte del cuerpo). Esto es un inconveniente menor de REPEAT con cursores: la comprobación no es tan limpia como con LOOP, donde puedes hacer FETCH + IF v_done THEN LEAVE sin necesidad del IF adicional.
Comparación de bucles
Los tres bucles de MySQL resuelven el mismo problema (repetir código) pero con filosofías diferentes:
| Característica | LOOP | WHILE | REPEAT |
|---|---|---|---|
| Condición | Explícita con LEAVE | Antes de iterar | Después de iterar con UNTIL |
| Ejecución mínima | 0 veces | 0 veces | 1 vez |
| Cuándo usar | Control total, cursores | Condición previa clara | Ejecutar al menos una vez |
| Equivalente en otros lenguajes | while(true) con break | while | do...while |
En la práctica, la elección entre los tres es principalmente una cuestión de legibilidad y de cuál expresa mejor tu intención. WHILE es el más usado para contadores y condiciones simples, LOOP es el preferido para cursores, y REPEAT es ideal cuando la primera ejecución siempre es necesaria.
Limpieza
DROP PROCEDURE IF EXISTS contar_repeat;
DROP PROCEDURE IF EXISTS repeat_una_vez;
DROP PROCEDURE IF EXISTS tabla_multiplicar;
DROP PROCEDURE IF EXISTS listar_productos_baratos;En el siguiente artículo veremos LEAVE, la sentencia para salir de un bucle o bloque.
Escrito por Eduardo Lázaro
