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:

  1. Variables con DECLARE
  2. Cursores con DECLARE CURSOR
  3. Handlers con DECLARE HANDLER
  4. 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();
nombrepreciorango_precio
ASUS ROG Zephyrus1899.99Muy alto
Lenovo ThinkPad X11549.00Muy alto
MacBook Air M31399.00Alto
iPhone 15 Pro1299.99Alto

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();
clientetotal_pedidostotal_gastadoticket_medio
María33899.971300.00
Carlos22399.981200.00
Ana21649.98825.00
Pedro21499.98750.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ísticaValor
Solo lecturaSí, no se puede usar UPDATE WHERE CURRENT OF
Solo avanceSí, no se puede retroceder
SensibleLos cambios en datos subyacentes pueden ser visibles
AnidableSí, con bloques BEGIN...END separados

Cuándo usar cursores

SituaciónUsar cursorAlternativa
Procesamiento fila por fila-
Lógica compleja por fila-
Operaciones en lote simplesNoUPDATE/INSERT con subconsulta
AgregacionesNoGROUP BY
Transformaciones simplesNoCASE 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