Ahora que MySQL 8.0 admite consultas recursivas , podemos decir que todas las bases de datos SQL populares admiten consultas recursivas en sintaxis estándar.
WITH RECURSIVE MyTree AS (
SELECT * FROM MyTable WHERE ParentId IS NULL
UNION ALL
SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;
Probé consultas recursivas en MySQL 8.0 en mi presentación Recursive Query Throwdown en 2017.
A continuación se muestra mi respuesta original de 2008:
Hay varias formas de almacenar datos estructurados en árbol en una base de datos relacional. Lo que muestra en su ejemplo utiliza dos métodos:
- Lista de adyacencia (la columna "padre") y
- Enumeración de ruta (los números de puntos en su columna de nombre).
Otra solución se llama Conjuntos anidados , y también se puede almacenar en la misma tabla. Lea " Árboles y jerarquías en SQL para Smarties " de Joe Celko para obtener más información sobre estos diseños.
Por lo general, prefiero un diseño llamado Tabla de cierre (también conocido como "Relación de adyacencia") para almacenar datos estructurados en árbol. Requiere otra tabla, pero luego consultar árboles es bastante fácil.
Cubro la Tabla de cierre en mi presentación Modelos para datos jerárquicos con SQL y PHP y en mi libro Antipatterns de SQL: Evitar las trampas de la programación de bases de datos .
CREATE TABLE ClosureTable (
ancestor_id INT NOT NULL REFERENCES FlatTable(id),
descendant_id INT NOT NULL REFERENCES FlatTable(id),
PRIMARY KEY (ancestor_id, descendant_id)
);
Almacene todas las rutas en la Tabla de cierre, donde hay una ascendencia directa de un nodo a otro. Incluya una fila para que cada nodo haga referencia a sí mismo. Por ejemplo, usando el conjunto de datos que mostró en su pregunta:
INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
(1,1), (1,2), (1,4), (1,6),
(2,2), (2,4),
(3,3), (3,5),
(4,4),
(5,5),
(6,6);
Ahora puede obtener un árbol que comienza en el nodo 1 como este:
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
El resultado (en el cliente MySQL) tiene el siguiente aspecto:
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
En otras palabras, los nodos 3 y 5 están excluidos, porque son parte de una jerarquía separada, que no desciende del nodo 1.
Re: comentario de e-satis sobre hijos inmediatos (o padres inmediatos). Puede agregar una path_length
columna " " a la ClosureTable
para facilitar la consulta específica de un hijo o padre inmediato (o cualquier otra distancia).
INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
(1,1,0), (1,2,1), (1,4,2), (1,6,1),
(2,2,0), (2,4,1),
(3,3,0), (3,5,1),
(4,4,0),
(5,5,0),
(6,6,0);
Luego, puede agregar un término en su búsqueda para consultar los elementos secundarios inmediatos de un nodo determinado. Estos son descendientes cuyo path_length
es 1.
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
AND path_length = 1;
+----+
| id |
+----+
| 2 |
| 6 |
+----+
Re comentario de @ashraf: "¿Qué tal si ordenamos todo el árbol [por nombre]?"
Aquí hay una consulta de ejemplo para devolver todos los nodos que son descendientes del nodo 1, unirlos a la FlatTable que contiene otros atributos de nodo como name
y ordenarlos por el nombre.
SELECT f.name
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;
Re comentar de @Nate:
SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id)
WHERE a.ancestor_id = 1
GROUP BY a.descendant_id
ORDER BY f.name
+------------+-------------+
| name | breadcrumbs |
+------------+-------------+
| Node 1 | 1 |
| Node 1.1 | 1,2 |
| Node 1.1.1 | 1,2,4 |
| Node 1.2 | 1,6 |
+------------+-------------+
Un usuario sugirió una edición hoy. Los moderadores de SO aprobaron la edición, pero la estoy revocando.
La edición sugirió que ORDER BY en la última consulta anterior debería ser ORDER BY b.path_length, f.name
, probablemente para asegurarse de que el orden coincida con la jerarquía. Pero esto no funciona, porque ordenaría "Nodo 1.1.1" después de "Nodo 1.2".
Si desea que el orden coincida con la jerarquía de una manera sensata, es posible, pero no simplemente ordenando por la longitud de la ruta. Por ejemplo, vea mi respuesta a la base de datos jerárquica de MySQL Closure Table: cómo extraer la información en el orden correcto .