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;| fecha | ventas_dia | num_pedidos |
|---|---|---|
| 2026-02-01 | 4523.50 | 12 |
| 2026-02-02 | 0.00 | 0 |
| 2026-02-03 | 1250.00 | 3 |
| 2026-02-04 | 8920.75 | 25 |
| ... | ... | ... |
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;| hora | pedidos_hora |
|---|---|
| 2026-02-14 00:00:00 | 2 |
| 2026-02-14 01:00:00 | 0 |
| 2026-02-14 02:00:00 | 0 |
| ... | ... |
| 2026-02-14 10:00:00 | 15 |
| 2026-02-14 11:00:00 | 23 |
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_dia | mes | ultimo_dia |
|---|---|---|
| 2026-01-01 | January 2026 | 2026-01-31 |
| 2026-02-01 | February 2026 | 2026-02-28 |
| 2026-03-01 | March 2026 | 2026-03-31 |
| ... | ... | ... |
| 2026-12-01 | December 2026 | 2026-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_name | Value |
|---|---|
| cte_max_recursion_depth | 1000 |
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
