Maintenant que MySQL 8.0 prend en charge les requêtes récursives , nous pouvons dire que toutes les bases de données SQL populaires prennent en charge les requêtes récursives dans la syntaxe standard.
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;
J'ai testé les requêtes récursives dans MySQL 8.0 dans ma présentation Recursive Query Throwdown en 2017.
Voici ma réponse originale de 2008:
Il existe plusieurs façons de stocker des données arborescentes dans une base de données relationnelle. Ce que vous montrez dans votre exemple utilise deux méthodes:
- Liste d'adjacence (la colonne "parent") et
- Énumération du chemin (les nombres en pointillés dans votre colonne de nom).
Une autre solution est appelée ensembles imbriqués et peut également être stockée dans la même table. Lisez " Arbres et hiérarchies en SQL pour Smarties " par Joe Celko pour plus d'informations sur ces conceptions.
Je préfère généralement une conception appelée Closure Table (aka "Adjacency Relation") pour stocker des données arborescentes. Il nécessite une autre table, mais interroger les arbres est assez facile.
Je couvre la table de fermeture dans ma présentation Modèles de données hiérarchiques avec SQL et PHP et dans mon livre Antipatterns SQL: éviter les pièges de la programmation de base de données .
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)
);
Stockez tous les chemins dans la table de fermeture, où il existe une ascendance directe d'un nœud à un autre. Inclure une ligne pour chaque nœud pour se référencer. Par exemple, en utilisant l'ensemble de données que vous avez montré dans votre question:
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);
Vous pouvez maintenant obtenir un arbre commençant au nœud 1 comme ceci:
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
La sortie (dans le client MySQL) ressemble à ceci:
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
En d'autres termes, les nœuds 3 et 5 sont exclus, car ils font partie d'une hiérarchie distincte, ne descendant pas du nœud 1.
Re: commentaire d'e-satis sur les enfants immédiats (ou le parent immédiat). Vous pouvez ajouter une path_length
colonne " " pour ClosureTable
faciliter la recherche spécifique d'un enfant ou d'un parent immédiat (ou de toute autre distance).
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);
Ensuite, vous pouvez ajouter un terme dans votre recherche pour interroger les enfants immédiats d'un nœud donné. Ce sont des descendants dont le path_length
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 |
+----+
Commentaire de @ashraf: "Que diriez-vous de trier tout l'arbre [par nom]?"
Voici un exemple de requête pour renvoyer tous les nœuds qui sont des descendants du nœud 1, les joindre à la FlatTable qui contient d'autres attributs de nœud tels que name
et trier par nom.
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 commentaire 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 utilisateur a suggéré une modification aujourd'hui. Les modérateurs ont approuvé la modification, mais je l'inverse.
L'édition a suggéré que l'ORDRE PAR dans la dernière requête ci-dessus soit ORDER BY b.path_length, f.name
, probablement pour s'assurer que la commande correspond à la hiérarchie. Mais cela ne fonctionne pas, car il ordonnerait "Node 1.1.1" après "Node 1.2".
Si vous voulez que le classement corresponde à la hiérarchie de manière sensée, c'est possible, mais pas simplement en triant par la longueur du chemin. Par exemple, voir ma réponse à la base de données hiérarchique MySQL Closure Table - Comment extraire les informations dans le bon ordre .