Cursores
Un cursor permite recorrer un conjunto de resultados fila por fila dentro de un procedimiento almacenado. Los cursores de MySQL son de solo lectura, no permiten desplazamiento hacia atrás y son sensibles a los datos subyacentes.
Sintaxis
-- 1. Declarar el cursor
DECLARE nombre_cursor CURSOR FOR sentencia_select;
-- 2. Declarar handler para fin de datos
DECLARE CONTINUE HANDLER FOR NOT FOUND SET variable = TRUE;
-- 3. Abrir el cursor
OPEN nombre_cursor;
-- 4. Leer filas
FETCH nombre_cursor INTO variable1, variable2, ...;
-- 5. Cerrar el cursor
CLOSE nombre_cursor;Orden de declaración
El orden dentro de un bloque BEGIN...END es estricto:
- Variables con
DECLARE - Cursores con
DECLARE CURSOR - Handlers con
DECLARE HANDLER - Sentencias ejecutables
Ejemplo básico
DELIMITER //
CREATE PROCEDURE listar_productos_cursor()
BEGIN
DECLARE v_nombre VARCHAR(100);
DECLARE v_precio DECIMAL(10,2);
DECLARE v_done INT DEFAULT FALSE;
DECLARE cur_productos CURSOR FOR
SELECT nombre, precio FROM productos
WHERE activo = TRUE AND precio > 1000
ORDER BY precio DESC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
DROP TEMPORARY TABLE IF EXISTS tmp_resultado;
CREATE TEMPORARY TABLE tmp_resultado (
nombre VARCHAR(100),
precio DECIMAL(10,2),
rango_precio VARCHAR(20)
);
OPEN cur_productos;
leer: LOOP
FETCH cur_productos INTO v_nombre, v_precio;
IF v_done THEN
LEAVE leer;
END IF;
INSERT INTO tmp_resultado VALUES (
v_nombre,
v_precio,
CASE
WHEN v_precio > 1500 THEN 'Muy alto'
ELSE 'Alto'
END
);
END LOOP leer;
CLOSE cur_productos;
SELECT * FROM tmp_resultado;
DROP TEMPORARY TABLE tmp_resultado;
END //
DELIMITER ;CALL listar_productos_cursor();| nombre | precio | rango_precio |
|---|---|---|
| ASUS ROG Zephyrus | 1899.99 | Muy alto |
| Lenovo ThinkPad X1 | 1549.00 | Muy alto |
| MacBook Air M3 | 1399.00 | Alto |
| iPhone 15 Pro | 1299.99 | Alto |
Handler NOT FOUND
El handler NOT FOUND se activa cuando FETCH intenta leer después de la última fila. Sin él, MySQL genera un error:
-- CONTINUE HANDLER: continúa la ejecución después de NOT FOUND
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
-- EXIT HANDLER: sale del bloque BEGIN...END actual
DECLARE EXIT HANDLER FOR NOT FOUND BEGIN END;CONTINUE HANDLER es el más utilizado porque permite verificar la variable de control en el bucle.
Cursor con cálculos acumulados
DELIMITER //
CREATE PROCEDURE resumen_ventas_por_cliente()
BEGIN
DECLARE v_cliente_id INT;
DECLARE v_nombre VARCHAR(100);
DECLARE v_total DECIMAL(10,2);
DECLARE v_pedidos INT;
DECLARE v_done INT DEFAULT FALSE;
DECLARE cur_clientes CURSOR FOR
SELECT c.id, c.nombre
FROM clientes c
WHERE EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.id)
ORDER BY c.nombre;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
DROP TEMPORARY TABLE IF EXISTS tmp_resumen;
CREATE TEMPORARY TABLE tmp_resumen (
cliente VARCHAR(100),
total_pedidos INT,
total_gastado DECIMAL(10,2),
ticket_medio DECIMAL(10,2)
);
OPEN cur_clientes;
leer: LOOP
FETCH cur_clientes INTO v_cliente_id, v_nombre;
IF v_done THEN
LEAVE leer;
END IF;
SELECT COUNT(*), COALESCE(SUM(total), 0)
INTO v_pedidos, v_total
FROM pedidos WHERE cliente_id = v_cliente_id;
INSERT INTO tmp_resumen VALUES (
v_nombre, v_pedidos, v_total,
ROUND(v_total / v_pedidos, 2)
);
END LOOP leer;
CLOSE cur_clientes;
SELECT * FROM tmp_resumen ORDER BY total_gastado DESC;
DROP TEMPORARY TABLE tmp_resumen;
END //
DELIMITER ;CALL resumen_ventas_por_cliente();| cliente | total_pedidos | total_gastado | ticket_medio |
|---|---|---|---|
| María | 3 | 3899.97 | 1300.00 |
| Carlos | 2 | 2399.98 | 1200.00 |
| Ana | 2 | 1649.98 | 825.00 |
| Pedro | 2 | 1499.98 | 750.00 |
| ... | ... | ... | ... |
Múltiples cursores
DELIMITER //
CREATE PROCEDURE comparar_categorias(IN cat1 INT, IN cat2 INT)
BEGIN
DECLARE v_nombre VARCHAR(100);
DECLARE v_precio DECIMAL(10,2);
DECLARE v_done1 INT DEFAULT FALSE;
DECLARE v_done2 INT DEFAULT FALSE;
DECLARE cur1 CURSOR FOR
SELECT nombre, precio FROM productos WHERE categoria_id = cat1;
DECLARE cur2 CURSOR FOR
SELECT nombre, precio FROM productos WHERE categoria_id = cat2;
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET v_done1 = TRUE;
-- Procesar primer cursor
OPEN cur1;
SELECT CONCAT('Categoría ', cat1) AS titulo;
leer1: LOOP
FETCH cur1 INTO v_nombre, v_precio;
IF v_done1 THEN LEAVE leer1; END IF;
SELECT v_nombre AS nombre, v_precio AS precio;
END LOOP leer1;
CLOSE cur1;
-- Reset handler y procesar segundo cursor
SET v_done1 = FALSE;
OPEN cur2;
SELECT CONCAT('Categoría ', cat2) AS titulo;
leer2: LOOP
FETCH cur2 INTO v_nombre, v_precio;
IF v_done1 THEN LEAVE leer2; END IF;
SELECT v_nombre AS nombre, v_precio AS precio;
END LOOP leer2;
CLOSE cur2;
END //
DELIMITER ;Características de los cursores en MySQL
| Característica | Valor |
|---|---|
| Solo lectura | Sí, no se puede usar UPDATE WHERE CURRENT OF |
| Solo avance | Sí, no se puede retroceder |
| Sensible | Los cambios en datos subyacentes pueden ser visibles |
| Anidable | Sí, con bloques BEGIN...END separados |
Cuándo usar cursores
| Situación | Usar cursor | Alternativa |
|---|---|---|
| Procesamiento fila por fila | Sí | - |
| Lógica compleja por fila | Sí | - |
| Operaciones en lote simples | No | UPDATE/INSERT con subconsulta |
| Agregaciones | No | GROUP BY |
| Transformaciones simples | No | CASE en SELECT |
Los cursores son más lentos que las operaciones basadas en conjuntos. Úsalos solo cuando la lógica por fila es demasiado compleja para expresar en SQL.
Limpieza
DROP PROCEDURE IF EXISTS listar_productos_cursor;
DROP PROCEDURE IF EXISTS resumen_ventas_por_cliente;
DROP PROCEDURE IF EXISTS comparar_categorias;En el siguiente artículo veremos cómo eliminar procedimientos con DROP PROCEDURE.
Escrito por Eduardo Lázaro
