Agora que o MySQL 8.0 suporta consultas recursivas , podemos dizer que todos os bancos de dados SQL populares suportam consultas recursivas na sintaxe padrão.
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;
Testei consultas recursivas no MySQL 8.0 na minha apresentação Recursive Query Throwdown em 2017.
Abaixo está a minha resposta original de 2008:
Existem várias maneiras de armazenar dados estruturados em árvore em um banco de dados relacional. O que você mostra no seu exemplo usa dois métodos:
- Lista de adjacências (a coluna "principal") e
- Enumeração de caminho (os números pontilhados na coluna do seu nome).
Outra solução é chamada Conjuntos Aninhados e também pode ser armazenada na mesma tabela. Leia " Árvores e hierarquias no SQL for Smarties ", de Joe Celko, para obter mais informações sobre esses designs.
Normalmente, eu prefiro um design chamado Closure Table (também conhecido como "Adjacency Relation") para armazenar dados estruturados em árvore. Requer outra tabela, mas é fácil consultar árvores.
Abordo a tabela de fechamento em minha apresentação Modelos para dados hierárquicos com SQL e PHP e em meu livro Antipatterns SQL: Evitando as armadilhas da programação de banco de dados .
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)
);
Armazene todos os caminhos na tabela de fechamento, onde há uma ascendência direta de um nó para outro. Inclua uma linha para cada nó para fazer referência a si próprio. Por exemplo, usando o conjunto de dados que você mostrou na sua pergunta:
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);
Agora você pode obter uma árvore começando no nó 1 como este:
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
A saída (no cliente MySQL) é semelhante à seguinte:
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
Em outras palavras, os nós 3 e 5 são excluídos porque fazem parte de uma hierarquia separada, não descendo do nó 1.
Re: comentário de e-satis sobre filhos imediatos (ou pais imediatos). Você pode adicionar uma path_length
coluna ao " " para ClosureTable
facilitar a consulta específica de um filho ou pai imediato (ou qualquer outra distância).
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);
Em seguida, você pode adicionar um termo na sua pesquisa para consultar os filhos imediatos de um determinado nó. Estes são descendentes cujo 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 |
+----+
Comentário de @ashraf: "Que tal classificar a árvore inteira [por nome]?"
Aqui está um exemplo de consulta para retornar todos os nós descendentes do nó 1, associá-los à FlatTable que contém outros atributos do nó, como name
, e classificar pelo 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;
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 |
+------------+-------------+
Um usuário sugeriu uma edição hoje. Os moderadores da SO aprovaram a edição, mas estou revertendo.
A edição sugeriu que ORDER BY na última consulta acima deveria ser ORDER BY b.path_length, f.name
, presumivelmente para garantir que a ordem corresponda à hierarquia. Mas isso não funciona, porque ordenaria "Nó 1.1.1" após "Nó 1.2".
Se você deseja que a ordem corresponda à hierarquia de maneira sensata, isso é possível, mas não simplesmente ordenando pelo comprimento do caminho. Por exemplo, veja minha resposta ao banco de dados hierárquico da Tabela de fechamento do MySQL - Como extrair informações na ordem correta .