Árboles y secuencias: WITH RECURSIVE
En resumen: WITH RECURSIVE es una consulta con un paso que lee su propio resultado, así que puede recorrer árboles como las categorías o generar una secuencia de fechas.
Problema
La ruta hasta la categoría
Encima de cada producto el sitio web muestra la ruta hasta él: «Alimentos > Lácteos». Las categorías son un árbol: cada una tiene un parent_id que apunta a la categoría de arriba. Con un self join del módulo sobre unir tablas se hace así:
Esto funciona porque el árbol tiene dos niveles. Si mañana aparece «Quesos» bajo «Lácteos», necesitas un join más, y para cuatro niveles otro más. No sabes de antemano cuántos niveles habrá.
La idea
Un paso que se lee a sí mismo
WITH RECURSIVE permite que un paso lea sus propias filas. Este es el ejemplo más pequeño, los números del 1 al 5:
El paso tiene dos partes, unidas por UNION ALL:
- El inicio
SELECT 1da la primera fila. - La parte recursiva
SELECT x + 1 FROM n WHERE x < 5toma las filas de la ronda anterior y crea otras nuevas: de 1 hace 2, de 2 hace 3.
Esto se repite hasta que la parte recursiva no devuelve nada: con x = 5 la condición ya no es verdadera. Un paso así se llama CTE recursiva.
Sin WHERE x < 5 la consulta no pararía nunca. Toda parte recursiva necesita una condición que tarde o temprano la detenga.
La idea
El árbol de categorías
Ahora la ruta. El inicio son las categorías principales (sin padre). La parte recursiva encuentra los hijos de todo lo encontrado hasta ahora y añade su nombre a la ruta:
En la primera ronda están «Alimentos» y «Hogar» (nivel 1). En la segunda están las seis subcategorías (nivel 2), cada una con la ruta de su padre, « > » y su propio nombre. En la tercera ronda ya no hay hijos y todo se detiene. Si el árbol se hace más profundo, la misma consulta llegará también a los niveles nuevos.
Pruébalo
Todo lo de «Alimentos»
Muestra el nombre y el precio de todos los productos de «Alimentos», junto con sus subcategorías. Obtén los números de categoría con un paso recursivo, no a mano. El campo empieza con el inicio del paso.
Solución
WITH RECURSIVE sub(id) AS (
SELECT id FROM categories WHERE name = 'Alimentos'
UNION ALL
SELECT c.id FROM categories c JOIN sub s ON c.parent_id = s.id
)
SELECT name, price FROM products
WHERE category_id IN (SELECT id FROM sub);El inicio es «Alimentos», la parte recursiva añade sus hijos, y los productos se encuentran con category_id IN (SELECT id FROM sub). 18 productos. «Alimentos» en sí no tiene productos, pero está en el paso y no estorba.
Míralo
Los días sin ventas
La gerencia quiere un gráfico de las ventas por día de octubre de 2026. El primer intento:
Cinco filas. Octubre tiene 31 días, y el gráfico uniría el 3 de octubre directamente con el 5 y escondería que del 7 al 17 de octubre no se vendió nada. Un día sin pedidos no está en orders, así que GROUP BY no tiene de qué crear una fila.
La idea
Una secuencia de fechas
Un paso recursivo puede crear todos los días por sí solo. El inicio es el 1 de octubre, y cada ronda añade un día con date(day, '+1 day'):
31 filas. Ahora las ventas se unen a los días con LEFT JOIN, y para los días sin pedidos COALESCE pone 0:
Están los 31 días, también los que tienen cero. El orden del LEFT JOIN importa: a la izquierda están los días, de los que quieres todos, a la derecha los pedidos, que pueden faltar.
Más precisamente, para curiosos
PostgreSQL tiene una función ya hecha para una secuencia: generate_series(DATE '2026-10-01', DATE '2026-10-31', INTERVAL '1 day'). En SQLite y en MySQL (desde la versión 8) una secuencia así se hace con WITH RECURSIVE, como aquí.
Ejercicio
Dos tareas
1. ¿En cuántos días de octubre de 2026 la tienda no tuvo ningún pedido?
Cuenta los días de octubre de 2026 sin un solo pedido, hasta el final del mes, incluidos los días posteriores al último pedido (columna quiet_days). El campo ya tiene la secuencia de días.
Solución
WITH RECURSIVE days(day) AS (
SELECT '2026-10-01'
UNION ALL
SELECT date(day, '+1 day') FROM days WHERE day < '2026-10-31'
)
SELECT COUNT(*) AS quiet_days FROM days d
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.ordered_at = d.day);WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.ordered_at = d.day) y COUNT(*): 26 días. Solo hay pedidos en 5 días.
2. El sitio web quiere la ruta completa de cada producto: «Alimentos > Lácteos > Queso blanco».
Muestra el nombre de cada producto que tiene categoría y su ruta completa (columna breadcrumb), desde la categoría principal hasta el nombre del producto. El campo ya tiene el árbol.
Solución
WITH RECURSIVE tree(id, name, path, level) AS (
SELECT id, name, name, 1
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, t.path || ' > ' || c.name, t.level + 1
FROM categories c
JOIN tree t ON c.parent_id = t.id
)
SELECT p.name, t.path || ' > ' || p.name AS breadcrumb
FROM products p
JOIN tree t ON t.id = p.category_id;JOIN tree t ON t.id = p.category_id y t.path || ' > ' || p.name. 24 filas: la tarjeta de regalo no tiene categoría y JOIN la deja fuera. Con LEFT JOIN estaría, pero con NULL como ruta.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: LEFT JOIN: también las filas sin pareja · Muchas tablas, y una tabla unida consigo misma · Subconsultas · Un informe por pasos: WITH
- Continúa en: Bases de datos de grafos
- Ejercicios sobre el tema: Subconsultas y ventanas