Ora che MySQL 8.0 supporta query ricorsive , possiamo dire che tutti i database SQL più diffusi supportano query ricorsive in sintassi 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;
Ho testato le query ricorsive in MySQL 8.0 nella mia presentazione Lancio di query ricorsive nel 2017.
Di seguito è la mia risposta originale del 2008:
Esistono diversi modi per archiviare i dati strutturati ad albero in un database relazionale. Ciò che mostri nel tuo esempio utilizza due metodi:
- Elenco di adiacenza (la colonna "padre") e
- Enumerazione percorso (i numeri punteggiati nella colonna del tuo nome).
Un'altra soluzione si chiama Set nidificati e può essere archiviata anche nella stessa tabella. Leggi " Alberi e gerarchie in SQL per Smarties " di Joe Celko per molte più informazioni su questi progetti.
Di solito preferisco un design chiamato Tabella di chiusura (noto anche come "relazione di adiacenza") per la memorizzazione di dati strutturati ad albero. Richiede un'altra tabella, ma eseguire query sugli alberi è piuttosto semplice.
Tratto Tabella di chiusura nella mia presentazione Modelli per dati gerarchici con SQL e PHP e nel mio libro SQL Antipatterns: evitare le insidie della programmazione di database .
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)
);
Memorizza tutti i percorsi nella Tabella di chiusura, dove esiste un antenato diretto da un nodo a un altro. Includere una riga per ogni nodo per fare riferimento a se stesso. Ad esempio, utilizzando il set di dati che hai mostrato nella tua domanda:
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);
Ora puoi ottenere un albero a partire dal nodo 1 in questo modo:
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
L'output (nel client MySQL) è simile al seguente:
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
In altre parole, i nodi 3 e 5 sono esclusi, perché fanno parte di una gerarchia separata, non discendente dal nodo 1.
Ri: commento di e-satis su figli immediati (o genitore immediato). È possibile aggiungere una path_length
colonna " " a ClosureTable
per semplificare l'interrogazione specifica per un figlio o un genitore immediato (o qualsiasi altra distanza).
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);
Quindi è possibile aggiungere un termine nella ricerca per interrogare i figli immediati di un determinato nodo. Questi sono i discendenti di cui 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 |
+----+
Riguardo al commento di @ashraf: "Che ne dici di ordinare l'intero albero [per nome]?"
Ecco una query di esempio per restituire tutti i nodi che sono discendenti del nodo 1, unirli alla FlatTable che contiene altri attributi di nodo come name
e ordinare in base al nome.
SELECT f.name
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;
Per commentare da @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 utente ha suggerito una modifica oggi. I moderatori SO hanno approvato la modifica, ma la sto invertendo.
La modifica ha suggerito che ORDER BY nell'ultima query sopra dovrebbe essere ORDER BY b.path_length, f.name
, presumibilmente per assicurarsi che l'ordinamento corrisponda alla gerarchia. Ma questo non funziona, perché ordinerebbe "Nodo 1.1.1" dopo "Nodo 1.2".
Se si desidera che l'ordinamento corrisponda alla gerarchia in modo ragionevole, ciò è possibile, ma non semplicemente ordinando in base alla lunghezza del percorso. Ad esempio, vedere la mia risposta al database gerarchico della tabella di chiusura di MySQL - Come estrarre le informazioni nell'ordine corretto .