¿Cómo crear una consulta recursiva jerárquica MySQL?
Tengo una tabla MySQL que es la siguiente:
identificación | nombre | Identificación de los padres |
---|---|---|
19 | Categoría 1 | 0 |
20 | categoría2 | 19 |
21 | categoría3 | 20 |
22 | categoría4 | 21 |
... | ... | ... |
Ahora, quiero tener una única consulta MySQL a la que simplemente le proporciono la identificación [por ejemplo, digamos id=19
] y luego debería obtener todas sus identificaciones secundarias [es decir, el resultado debe tener las identificaciones '20,21,22']....
Se desconoce la jerarquía de los niños; puede variar....
Sé cómo hacerlo usando un for
bucle... pero ¿cómo lograr lo mismo usando una única consulta MySQL?
Para MySQL 8+: use la sintaxis recursiva with
.
Para MySQL 5.x: use variables en línea, ID de ruta o autouniones.
mysql 8+
with recursive cte (id, name, parent_id) as (
select id,
name,
parent_id
from products
where parent_id = 19
union all
select p.id,
p.name,
p.parent_id
from products p
inner join cte
on p.parent_id = cte.id
)
select * from cte;
El valor especificado en parent_id = 19
debe establecerse en el id
del padre del que desea seleccionar todos los descendientes.
MySQL 5.x
Para las versiones de MySQL que no admiten expresiones de tabla comunes (hasta la versión 5.7), esto se puede lograr con la siguiente consulta:
select id,
name,
parent_id
from (select * from products
order by parent_id, id) products_sorted,
(select @pv := '19') initialisation
where find_in_set(parent_id, @pv)
and length(@pv := concat(@pv, ',', id))
Aquí hay un violín .
Aquí, el valor especificado en @pv := '19'
debe establecerse en el id
del padre del que desea seleccionar todos los descendientes.
Esto también funcionará si un padre tiene varios hijos. Sin embargo, se requiere que cada registro cumpla la condición parent_id < id
, de lo contrario los resultados no estarán completos.
Asignaciones de variables dentro de una consulta
Esta consulta utiliza una sintaxis específica de MySQL: las variables se asignan y modifican durante su ejecución. Se hacen algunas suposiciones sobre el orden de ejecución:
- La
from
cláusula se evalúa primero. Ahí es donde@pv
se inicializa. - La
where
cláusula se evalúa para cada registro en el orden de recuperación de losfrom
alias. Entonces, aquí es donde se establece una condición para incluir solo registros para los cuales el padre ya estaba identificado como en el árbol de descendientes (todos los descendientes del padre principal se agregan progresivamente@pv
). - Las condiciones de esta
where
cláusula se evalúan en orden y la evaluación se interrumpe una vez que el resultado total es seguro. Por lo tanto, la segunda condición debe estar en segundo lugar, ya que agregaid
a la lista principal, y esto solo debería suceder siid
pasa la primera condición. Lalength
función solo se llama para garantizar que esta condición sea siempre verdadera, incluso si lapv
cadena por alguna razón arrojara un valor falso.
En definitiva, uno puede considerar que estas suposiciones son demasiado arriesgadas como para confiar en ellas. La documentación advierte:
es posible que obtenga los resultados que espera, pero esto no está garantizado [...] el orden de evaluación de las expresiones que involucran variables de usuario no está definido.
Entonces, aunque funciona de manera consistente con la consulta anterior, el orden de evaluación aún puede cambiar, por ejemplo, cuando agrega condiciones o usa esta consulta como vista o subconsulta en una consulta más grande. Es una "característica" que se eliminará en una futura versión de MySQL :
Las versiones anteriores de MySQL permitieron asignar un valor a una variable de usuario en declaraciones distintas a
SET
. Esta funcionalidad es compatible con MySQL 8.0 por compatibilidad con versiones anteriores, pero está sujeta a eliminación en una versión futura de MySQL.
Como se indicó anteriormente, desde MySQL 8.0 en adelante debes usar la with
sintaxis recursiva.
Eficiencia
Para conjuntos de datos muy grandes, esta solución puede resultar lenta, ya que la find_in_set
operación no es la forma más ideal de encontrar un número en una lista, y menos aún en una lista que alcanza un tamaño del mismo orden de magnitud que el número de registros devueltos.
Alternativa 1: with recursive
,connect by
Cada vez más bases de datos implementan la sintaxis estándar ISO SQL:1999WITH [RECURSIVE]
para consultas recursivas (por ejemplo, Postgres 8.4+ , SQL Server 2005+ , DB2 , Oracle 11gR2+ , SQLite 3.8.4+ , Firebird 2.1+ , H2 , HyperSQL 2.1.0+ , Teradata , MaríaDB 10.2.2+ ). Y a partir de la versión 8.0, MySQL también lo admite . Consulte la parte superior de esta respuesta para conocer la sintaxis a utilizar.
Algunas bases de datos tienen una sintaxis alternativa no estándar para búsquedas jerárquicas, como la CONNECT BY
cláusula disponible en Oracle , DB2 , Informix , CUBRID y otras bases de datos.
La versión 5.7 de MySQL no ofrece dicha característica. Cuando su motor de base de datos proporciona esta sintaxis o puede migrar a uno que la proporcione, entonces esa es sin duda la mejor opción. Si no es así, considere también las siguientes alternativas.
Alternativa 2: Identificadores de estilo de ruta
Las cosas se vuelven mucho más fáciles si asigna id
valores que contengan la información jerárquica: una ruta. Por ejemplo, en su caso esto podría verse así:
IDENTIFICACIÓN | NOMBRE |
---|---|
19 | Categoría 1 |
19/1 | categoría2 |
19/1/1 | categoría3 |
19/1/1/1 | categoría4 |
Entonces tu select
lucirías así:
select id,
name
from products
where id like '19/%'
Alternativa 3: Autouniones repetidas
Si conoce un límite superior de cuán profundo puede llegar a ser su árbol jerárquico, puede utilizar una sql
consulta estándar como esta:
select p6.parent_id as parent6_id,
p5.parent_id as parent5_id,
p4.parent_id as parent4_id,
p3.parent_id as parent3_id,
p2.parent_id as parent2_id,
p1.parent_id as parent_id,
p1.id as product_id,
p1.name
from products p1
left join products p2 on p2.id = p1.parent_id
left join products p3 on p3.id = p2.parent_id
left join products p4 on p4.id = p3.parent_id
left join products p5 on p5.id = p4.parent_id
left join products p6 on p6.id = p5.parent_id
where 19 in (p1.parent_id,
p2.parent_id,
p3.parent_id,
p4.parent_id,
p5.parent_id,
p6.parent_id)
order by 1, 2, 3, 4, 5, 6, 7;
Mira este violín
La where
condición especifica de qué padre desea recuperar los descendientes. Puede ampliar esta consulta con más niveles según sea necesario.
Del blog Gestión de datos jerárquicos en MySQL
Estructura de la mesa
+-------------+----------------------+--------+
| category_id | name | parent |
+-------------+----------------------+--------+
| 1 | ELECTRONICS | NULL |
| 2 | TELEVISIONS | 1 |
| 3 | TUBE | 2 |
| 4 | LCD | 2 |
| 5 | PLASMA | 2 |
| 6 | PORTABLE ELECTRONICS | 1 |
| 7 | MP3 PLAYERS | 6 |
| 8 | FLASH | 7 |
| 9 | CD PLAYERS | 6 |
| 10 | 2 WAY RADIOS | 6 |
+-------------+----------------------+--------+
Consulta:
SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS';
Producción
+-------------+----------------------+--------------+-------+
| lev1 | lev2 | lev3 | lev4 |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS | TUBE | NULL |
| ELECTRONICS | TELEVISIONS | LCD | NULL |
| ELECTRONICS | TELEVISIONS | PLASMA | NULL |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS | NULL |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL |
+-------------+----------------------+--------------+-------+
La mayoría de los usuarios en un momento u otro han tratado con datos jerárquicos en una base de datos SQL y sin duda han aprendido que la gestión de datos jerárquicos no es para lo que está diseñada una base de datos relacional. Las tablas de una base de datos relacional no son jerárquicas (como XML), sino que son simplemente una lista plana. Los datos jerárquicos tienen una relación padre-hijo que no se representa de forma natural en una tabla de base de datos relacional. Leer más
Consulte el blog para obtener más detalles.
EDITAR:
select @pv:=category_id as category_id, name, parent from category
join
(select @pv:=19)tmp
where parent=@pv
Producción:
category_id name parent
19 category1 0
20 category2 19
21 category3 20
22 category4 21
Referencia: ¿Cómo hacer la consulta SELECT recursiva en Mysql?
Pruebe estos:
Definición de tabla:
DROP TABLE IF EXISTS category;
CREATE TABLE category (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20),
parent_id INT,
CONSTRAINT fk_category_parent FOREIGN KEY (parent_id)
REFERENCES category (id)
) engine=innodb;
Filas experimentales:
INSERT INTO category VALUES
(19, 'category1', NULL),
(20, 'category2', 19),
(21, 'category3', 20),
(22, 'category4', 21),
(23, 'categoryA', 19),
(24, 'categoryB', 23),
(25, 'categoryC', 23),
(26, 'categoryD', 24);
Procedimiento almacenado recursivo:
DROP PROCEDURE IF EXISTS getpath;
DELIMITER $$
CREATE PROCEDURE getpath(IN cat_id INT, OUT path TEXT)
BEGIN
DECLARE catname VARCHAR(20);
DECLARE temppath TEXT;
DECLARE tempparent INT;
SET max_sp_recursion_depth = 255;
SELECT name, parent_id FROM category WHERE id=cat_id INTO catname, tempparent;
IF tempparent IS NULL
THEN
SET path = catname;
ELSE
CALL getpath(tempparent, temppath);
SET path = CONCAT(temppath, '/', catname);
END IF;
END$$
DELIMITER ;
Función contenedora para el procedimiento almacenado:
DROP FUNCTION IF EXISTS getpath;
DELIMITER $$
CREATE FUNCTION getpath(cat_id INT) RETURNS TEXT DETERMINISTIC
BEGIN
DECLARE res TEXT;
CALL getpath(cat_id, res);
RETURN res;
END$$
DELIMITER ;
Seleccione ejemplo:
SELECT id, name, getpath(id) AS path FROM category;
Producción:
+----+-----------+-----------------------------------------+
| id | name | path |
+----+-----------+-----------------------------------------+
| 19 | category1 | category1 |
| 20 | category2 | category1/category2 |
| 21 | category3 | category1/category2/category3 |
| 22 | category4 | category1/category2/category3/category4 |
| 23 | categoryA | category1/categoryA |
| 24 | categoryB | category1/categoryA/categoryB |
| 25 | categoryC | category1/categoryA/categoryC |
| 26 | categoryD | category1/categoryA/categoryB/categoryD |
+----+-----------+-----------------------------------------+
Filtrar filas con cierta ruta:
SELECT id, name, getpath(id) AS path FROM category HAVING path LIKE 'category1/category2%';
Producción:
+----+-----------+-----------------------------------------+
| id | name | path |
+----+-----------+-----------------------------------------+
| 20 | category2 | category1/category2 |
| 21 | category3 | category1/category2/category3 |
| 22 | category4 | category1/category2/category3/category4 |
+----+-----------+-----------------------------------------+
Si necesita una velocidad de lectura rápida, la mejor opción es utilizar una tabla de cierre. Una tabla de cierre contiene una fila para cada par de antepasados/descendientes. Entonces, en su ejemplo, la tabla de cierre se vería así
ancestor | descendant | depth
0 | 0 | 0
0 | 19 | 1
0 | 20 | 2
0 | 21 | 3
0 | 22 | 4
19 | 19 | 0
19 | 20 | 1
19 | 21 | 3
19 | 22 | 4
20 | 20 | 0
20 | 21 | 1
20 | 22 | 2
21 | 21 | 0
21 | 22 | 1
22 | 22 | 0
Una vez que tenga esta tabla, las consultas jerárquicas se vuelven muy fáciles y rápidas. Para conseguir todos los descendientes de la categoría 20:
SELECT cat.* FROM categories_closure AS cl
INNER JOIN categories AS cat ON cat.id = cl.descendant
WHERE cl.ancestor = 20 AND cl.depth > 0
Por supuesto, existe una gran desventaja cuando se utilizan datos desnormalizados como este. Debe mantener la tabla de cierre junto con la tabla de categorías. Probablemente la mejor manera sea utilizar activadores, pero es algo complejo realizar un seguimiento correcto de las inserciones/actualizaciones/eliminaciones de las tablas de cierre. Como ocurre con todo, debe analizar sus necesidades y decidir qué enfoque es mejor para usted.
Editar : consulte la pregunta ¿Cuáles son las opciones para almacenar datos jerárquicos en una base de datos relacional? para más opciones. Existen diferentes soluciones óptimas para diferentes situaciones.
Basado en la respuesta de @trincot, muy bien explicada, uso WITH RECURSIVE ()
una declaración para crear una ruta de navegación usando id
la página actual y retrocedo en la jerarquía para encontrar cada parent
en mi route
tabla.
Entonces, la solución @trincot se adapta aquí en la dirección opuesta para encontrar padres en lugar de descendientes.
También agregué depth
valor que es útil para invertir el orden de los resultados (de lo contrario, la ruta de navegación estaría al revés).
WITH RECURSIVE cte (
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent_id`,
`depth`
) AS (
SELECT
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent_id`,
1 AS `depth`
FROM `route`
WHERE `id` = :id
UNION ALL
SELECT
P.`id`,
P.`title`,
P.`url`,
P.`icon`,
P.`class`,
P.`parent_id`,
`depth` + 1
FROM `route` P
INNER JOIN cte
ON P.`id` = cte.`parent_id`
)
SELECT * FROM cte ORDER BY `depth` DESC;
Antes de actualizar a mySQL 8+, estaba usando vars pero está en desuso y ya no funciona en mi versión 8.0.22 .
EDITAR 2021-02-19 : Ejemplo de menú jerárquico
Después del comentario de @david, decidí intentar hacer un menú jerárquico completo con todos los nodos y ordenados como quiero (con sorting
columnas que clasifican los elementos en cada profundidad). Muy útil para mi página de matriz de usuario/autorización.
Esto realmente simplifica mi versión anterior con una consulta en cada profundidad (bucles PHP) .
Este ejemplo integra un INNER JOIN con url
una tabla para filtrar la ruta por sitio web (sistema CMS de múltiples sitios web).
Puede ver la path
columna esencial que contiene CONCAT()
la función para ordenar el menú de la manera correcta.
SELECT R.* FROM (
WITH RECURSIVE cte (
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent`,
`depth`,
`sorting`,
`path`
) AS (
SELECT
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent`,
1 AS `depth`,
`sorting`,
CONCAT(`sorting`, ' ' , `title`) AS `path`
FROM `route`
WHERE `parent` = 0
UNION ALL SELECT
D.`id`,
D.`title`,
D.`url`,
D.`icon`,
D.`class`,
D.`parent`,
`depth` + 1,
D.`sorting`,
CONCAT(cte.`path`, ' > ', D.`sorting`, ' ' , D.`title`)
FROM `route` D
INNER JOIN cte
ON cte.`id` = D.`parent`
)
SELECT * FROM cte
) R
INNER JOIN `url` U
ON R.`id` = U.`route_id`
AND U.`site_id` = 1
ORDER BY `path` ASC