Secuencias

En muchas situaciones necesitas generar una serie de números o fechas consecutivas sin que esos datos existan en ninguna tabla: rellenar huecos en un informe de ventas diarias, generar rangos de fechas para un calendario, crear filas de prueba, o numerar resultados secuencialmente. MySQL no tiene una función GENERATE_SERIES() como PostgreSQL, pero las expresiones de tabla comunes recursivas (recursive CTEs), disponibles desde MySQL 8.0, resuelven este problema de forma elegante.

Generar una secuencia numérica con CTE recursivo

Un CTE recursivo se compone de un caso base y una parte recursiva unidos por UNION ALL:

WITH RECURSIVE secuencia AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1
    FROM secuencia
    WHERE n < 10
)
SELECT n FROM secuencia;
n
1
2
3
4
5
6
7
8
9
10

La primera parte (SELECT 1 AS n) establece el valor inicial. La parte recursiva (SELECT n + 1 FROM secuencia WHERE n < 10) genera el siguiente número sumando 1 al anterior, deteniéndose cuando n alcanza 10.

Secuencias con inicio y fin personalizados

Puedes ajustar el rango cambiando el valor inicial y la condición:

WITH RECURSIVE secuencia AS (
    SELECT 100 AS n
    UNION ALL
    SELECT n + 5
    FROM secuencia
    WHERE n < 150
)
SELECT n FROM secuencia;
n
100
105
110
115
120
125
130
135
140
145
150

Este ejemplo genera una secuencia de 100 a 150 con incrementos de 5.

Generar un rango de fechas

Una de las aplicaciones más prácticas de las secuencias es generar rangos de fechas. Esto es esencial para informes que necesitan mostrar todas las fechas, incluso aquellas sin datos:

WITH RECURSIVE fechas AS (
    SELECT DATE('2026-02-01') AS fecha
    UNION ALL
    SELECT fecha + INTERVAL 1 DAY
    FROM fechas
    WHERE fecha < '2026-02-28'
)
SELECT fecha FROM fechas;
fecha
2026-02-01
2026-02-02
2026-02-03
...
2026-02-28

Este CTE genera cada día de febrero de 2026.

Caso práctico: ventas diarias sin huecos

Supongamos que tienes una tabla de pedidos y quieres un informe de ventas diarias. Algunos días no tienen ventas y normalmente se omitirían del resultado. Con una secuencia de fechas puedes asegurar que todos los días aparezcan:

WITH RECURSIVE calendario AS (
    SELECT DATE('2026-02-01') AS fecha
    UNION ALL
    SELECT fecha + INTERVAL 1 DAY
    FROM calendario
    WHERE fecha < '2026-02-28'
)
SELECT
    c.fecha,
    COALESCE(SUM(p.total), 0) AS ventas_dia,
    COALESCE(COUNT(p.id), 0) AS num_pedidos
FROM calendario c
LEFT JOIN pedidos p ON DATE(p.fecha_pedido) = c.fecha
GROUP BY c.fecha
ORDER BY c.fecha;
fechaventas_dianum_pedidos
2026-02-014523.5012
2026-02-020.000
2026-02-031250.003
2026-02-048920.7525
.........

El LEFT JOIN asegura que las fechas sin pedidos aparezcan con valores cero gracias a COALESCE.

Generar rangos de horas

Para informes por hora, genera secuencias de marcas de tiempo:

WITH RECURSIVE horas AS (
    SELECT CAST('2026-02-14 00:00:00' AS DATETIME) AS hora
    UNION ALL
    SELECT hora + INTERVAL 1 HOUR
    FROM horas
    WHERE hora < '2026-02-14 23:00:00'
)
SELECT
    h.hora,
    COALESCE(COUNT(p.id), 0) AS pedidos_hora
FROM horas h
LEFT JOIN pedidos p ON p.fecha_pedido >= h.hora
    AND p.fecha_pedido < h.hora + INTERVAL 1 HOUR
GROUP BY h.hora
ORDER BY h.hora;
horapedidos_hora
2026-02-14 00:00:002
2026-02-14 01:00:000
2026-02-14 02:00:000
......
2026-02-14 10:00:0015
2026-02-14 11:00:0023

Generar meses de un año

WITH RECURSIVE meses AS (
    SELECT DATE('2026-01-01') AS primer_dia
    UNION ALL
    SELECT primer_dia + INTERVAL 1 MONTH
    FROM meses
    WHERE primer_dia < '2026-12-01'
)
SELECT
    primer_dia,
    DATE_FORMAT(primer_dia, '%M %Y') AS mes,
    LAST_DAY(primer_dia) AS ultimo_dia
FROM meses;
primer_diamesultimo_dia
2026-01-01January 20262026-01-31
2026-02-01February 20262026-02-28
2026-03-01March 20262026-03-31
.........
2026-12-01December 20262026-12-31

Numeración de filas como secuencia

Puedes usar un CTE recursivo para generar números que luego se asocian con filas de otra tabla:

WITH RECURSIVE numeros AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM numeros WHERE n < 100
)
SELECT
    n AS posicion,
    p.nombre,
    p.precio
FROM numeros n
JOIN (
    SELECT nombre, precio, ROW_NUMBER() OVER (ORDER BY precio DESC) AS rn
    FROM productos
) p ON n.n = p.rn
WHERE n <= 10;

En la práctica, ROW_NUMBER() es más directo para este caso, pero las secuencias son útiles cuando necesitas combinar con datos de formas más complejas.

Generar datos de prueba

Las secuencias son muy útiles para generar datos de prueba rápidamente:

WITH RECURSIVE datos AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM datos WHERE n < 1000
)
INSERT INTO productos_test (nombre, precio, stock)
SELECT
    CONCAT('Producto de prueba #', n),
    ROUND(RAND() * 1000, 2),
    FLOOR(RAND() * 500)
FROM datos;

Este INSERT genera 1000 productos con nombres secuenciales, precios aleatorios entre 0 y 1000, y stocks aleatorios entre 0 y 500.

Límite de recursión

MySQL tiene un límite de profundidad de recursión controlado por cte_max_recursion_depth. El valor por defecto es 1000:

SHOW VARIABLES LIKE 'cte_max_recursion_depth';
Variable_nameValue
cte_max_recursion_depth1000

Si necesitas secuencias más largas, aumenta el límite:

-- Aumentar para la sesión actual
SET SESSION cte_max_recursion_depth = 10000;
 
-- Generar secuencia de 10,000 números
WITH RECURSIVE grande AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM grande WHERE n < 10000
)
SELECT COUNT(*) FROM grande;

Para secuencias muy grandes (millones), un CTE recursivo puede ser lento. En esos casos, considera crear una tabla de números permanente.

Tabla de números permanente

Si usas secuencias numéricas frecuentemente, crear una tabla permanente es más eficiente:

CREATE TABLE numeros (n INT PRIMARY KEY);
 
-- Poblar con CTE recursivo
SET SESSION cte_max_recursion_depth = 100000;
 
INSERT INTO numeros
WITH RECURSIVE seq AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM seq WHERE n < 100000
)
SELECT n FROM seq;

Con esta tabla disponible, generar secuencias de fechas o números es una simple consulta:

-- Generar todas las fechas de 2026
SELECT DATE('2026-01-01') + INTERVAL (n - 1) DAY AS fecha
FROM numeros
WHERE n <= 365;

Secuencias descendentes

Para generar secuencias en orden inverso, simplemente invierte la lógica:

WITH RECURSIVE cuenta_regresiva AS (
    SELECT 10 AS n
    UNION ALL
    SELECT n - 1
    FROM cuenta_regresiva
    WHERE n > 1
)
SELECT n FROM cuenta_regresiva;
n
10
9
8
...
1

Las secuencias generadas con CTEs recursivos son una herramienta versátil que suple la falta de una función GENERATE_SERIES() nativa en MySQL. Desde rellenar huecos en informes hasta generar datos de prueba, estas técnicas te permiten trabajar con conjuntos de datos secuenciales sin necesidad de tablas auxiliares.

Escrito por Eduardo Lázaro