Suponha que você tenha uma tabela plana que armazene uma hierarquia de árvore ordenada:
Id Name ParentId Order
1 'Node 1' 0 10
2 'Node 1.1' 1 10
3 'Node 2' 0 20
4 'Node 1.1.1' 2 10
5 'Node 2.1' 3 10
6 'Node 1.2' 1 20
Aqui está um diagrama, onde temos [id] Name
. O nó raiz 0 é fictício.
[0] RAIZ / \ [1] Nó 1 [3] Nó 2 / \ \ [2] Nó 1,1 [6] Nó 1,2 [5] Nó 2,1 / [4] Nó 1.1.1.
Que abordagem minimalista você usaria para produzir isso em HTML (ou texto, por sinal) como uma árvore recortada e ordenada corretamente?
Suponha ainda que você tenha apenas estruturas básicas de dados (matrizes e hashmaps), sem objetos sofisticados com referências de pai / filho, sem ORM, sem estrutura, apenas com as duas mãos. A tabela é representada como um conjunto de resultados, que pode ser acessado aleatoriamente.
Pseudo-código ou inglês comum é bom, essa é uma questão puramente conceitual.
Pergunta de bônus: Existe uma maneira fundamentalmente melhor de armazenar uma estrutura de árvore como esta em um RDBMS?
EDITAS E ADIÇÕES
Para responder à pergunta de um comentarista ( Mark Bessey ): Um nó raiz não é necessário, porque nunca será exibido de qualquer maneira. ParentId = 0 é a convenção para expressar "estes são de nível superior". A coluna Ordem define como os nós com o mesmo pai serão classificados.
O "conjunto de resultados" de que falei pode ser representado como uma matriz de hashmaps (para permanecer nessa terminologia). Para o meu exemplo era para já estar lá. Algumas respostas vão além e constroem primeiro, mas tudo bem.
A árvore pode ser arbitrariamente profunda. Cada nó pode ter N filhos. Eu não tinha exatamente uma árvore "milhões de entradas" em mente.
Não confunda minha escolha de nomeação de nó ('Nó 1.1.1') com algo em que confiar. Os nós poderiam igualmente ser chamados de 'Frank' ou 'Bob', nenhuma estrutura de nomenclatura está implícita, isso era apenas para torná-la legível.
Eu publiquei minha própria solução para que vocês possam fazer isso em pedaços.
Respostas:
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.
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:
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 .
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:
Agora você pode obter uma árvore começando no nó 1 como este:
A saída (no cliente MySQL) é semelhante à seguinte:
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 " " paraClosureTable
facilitar a consulta específica de um filho ou pai imediato (ou qualquer outra distância).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.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.Re comentar de @Nate:
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 .
fonte
parent_id
) possui apenas uma linha para expressar cada relacionamento pai-filho, mas a Tabela de fechamento possui muitas.Se você usar conjuntos aninhados (às vezes chamados de Traversal de Árvore de Pré-encomenda Modificado), poderá extrair toda a estrutura de árvore ou qualquer subárvore dentro dela em ordem de árvore com uma única consulta, com o custo de inserções mais caras, conforme necessário. gerenciar colunas que descrevem um caminho em ordem através da estrutura da árvore.
Para django-mptt , usei uma estrutura como esta:
Que descreve uma árvore que se parece com isso (com a
id
representação de cada item):Ou, como um diagrama de conjuntos aninhados, que torna mais óbvio como os valores
lft
erght
funcionam:Como você pode ver, para obter toda a subárvore para um determinado nó, na ordem de árvore, você simplesmente tem que selecionar todas as linhas que têm
lft
erght
valores entre os seuslft
erght
valores. Também é simples recuperar a árvore dos antepassados para um determinado nó.A
level
coluna é um pouco de desnormalização por conveniência, mais do que tudo, etree_id
permite que você reinicie a numeraçãolft
erght
cada nó de nível superior, o que reduz o número de colunas afetadas por inserções, movimentos e exclusões, pois as colunaslft
erght
devem ser ajustado de acordo quando essas operações ocorrem, a fim de criar ou fechar lacunas. Fiz algumas anotações de desenvolvimento no momento em que estava tentando entender as consultas necessárias para cada operação.Em termos de realmente trabalhar com esses dados para exibir uma árvore, criei uma
tree_item_iterator
função de utilitário que, para cada nó, deveria fornecer informações suficientes para gerar o tipo de exibição desejado.Mais informações sobre MPTT:
fonte
lft
erght
para nomes de colunas, quero dizer quantos caracteres não precisamos digitar? 1?!É uma pergunta bastante antiga, mas, como tem muitas visões, acho que vale a pena apresentar uma alternativa e, na minha opinião, uma solução muito elegante.
Para ler uma estrutura em árvore, você pode usar CTEs (Common Table Expressions ) recursivas . Ele oferece a possibilidade de buscar toda a estrutura da árvore de uma só vez, ter informações sobre o nível do nó, seu nó pai e a ordem dentro dos filhos do nó pai.
Deixe-me mostrar como isso funcionaria no PostgreSQL 9.1.
Crie uma estrutura
Escreva uma consulta
Aqui estão os resultados:
Os nós da árvore são ordenados por um nível de profundidade. No resultado final, apresentá-los-emos nas linhas subseqüentes.
Para cada nível, eles são ordenados por parent_id e node_order no pai. Isso nos diz como apresentá-los no nó do link de saída ao pai nesta ordem.
Tendo essa estrutura, não seria difícil fazer uma apresentação muito boa em HTML.
CTEs recursivos estão disponíveis no PostgreSQL, IBM DB2, MS SQL Server e Oracle .
Se você quiser ler mais sobre consultas SQL recursivas, verifique a documentação do seu DBMS favorito ou leia meus dois artigos que abordam este tópico:
fonte
No Oracle 9i, você pode usar CONNECT BY.
No SQL Server 2005, você pode usar uma expressão de tabela comum recursiva (CTE).
Ambos produzirão os seguintes resultados.
fonte
A resposta de Bill é muito boa, essa resposta adiciona algumas coisas que me fazem desejar respostas encadeadas com suporte.
Enfim, eu queria apoiar a estrutura em árvore e a propriedade Order. Incluí uma única propriedade em cada Nó chamado
leftSibling
que faz o mesmoOrder
na pergunta original (manter a ordem da esquerda para a direita).Mais detalhes e código SQL no meu blog .
Obrigado Bill, sua resposta foi útil para começar!
fonte
Bem, dada a escolha, eu estaria usando objetos. Eu criaria um objeto para cada registro em que cada objeto tem uma
children
coleção e os armazenaria todos em uma matriz assoc (/ hashtable) em que o ID é a chave. E percorra a coleção uma vez, adicionando os filhos aos campos filhos relevantes. Simples.Mas como você não está se divertindo ao restringir o uso de uma boa OOP, eu provavelmente iteraria com base em:
Edit: isso é semelhante a algumas outras entradas, mas acho que é um pouco mais limpo. Uma coisa que vou acrescentar: isso é extremamente intensivo em SQL. É desagradável . Se você tiver escolha, siga a rota OOP.
fonte
Isso foi escrito rapidamente, e não é bonito nem eficiente (além disso, ele autoboxe muito, convertendo entre
int
eInteger
é chato!), Mas funciona.Provavelmente quebra as regras, já que estou criando meus próprios objetos, mas ei, estou fazendo isso como uma diversão do trabalho real :)
Isso também pressupõe que o resultSet / table seja completamente lido em algum tipo de estrutura antes de você começar a construir Nós, o que não seria a melhor solução se você tivesse centenas de milhares de linhas.
fonte
Existem realmente boas soluções que exploram a representação btree interna dos índices sql. Isso se baseia em algumas ótimas pesquisas feitas por volta de 1998.
Aqui está uma tabela de exemplo (no mysql).
Os únicos campos necessários para a representação em árvore são:
Aqui está um exemplo de população de 24 nós, ordenada por tw:
Todo resultado da árvore pode ser feito de forma não recursiva. Por exemplo, para obter uma lista de ancestrais do nó em tw = '22 '
Antepassados
Irmãos e filhos são triviais - basta usar a ordenação do campo pa por dois.
Descendentes
Por exemplo, o conjunto (ramificação) de nós com raiz em tw = 17.
Notas Adicionais
Essa metodologia é extremamente útil para quando há um número muito maior de leituras do que inserções ou atualizações.
Como a inserção, movimento ou atualização de um nó na árvore requer que a árvore seja ajustada, é necessário bloquear a tabela antes de iniciar a ação.
O custo de inserção / exclusão é alto porque os valores de índice tw e sz (tamanho da ramificação) precisam ser atualizados em todos os nós após o ponto de inserção e para todos os ancestrais, respectivamente.
A movimentação de ramificação envolve mover o valor tw da ramificação para fora do intervalo, portanto, também é necessário desativar as restrições de chave estrangeira ao mover uma ramificação. Existem basicamente quatro consultas necessárias para mover uma ramificação:
Ajustar consultas em árvore
A abertura / fechamento de lacunas na árvore é uma subfunção importante usada pelos métodos de criação / atualização / exclusão, por isso a incluo aqui.
Precisamos de dois parâmetros - um sinalizador representando se estamos diminuindo ou diminuindo o tamanho e o índice tw do nó. Portanto, por exemplo tw = 18 (que tem um tamanho de ramificação de 5). Vamos supor que estamos reduzindo o tamanho (removendo tw) - isso significa que estamos usando '-' em vez de '+' nas atualizações do exemplo a seguir.
Primeiro usamos uma função ancestral (ligeiramente alterada) para atualizar o valor sz.
Então precisamos ajustar o tw para aqueles cujo tw é maior que o ramo a ser removido.
Em seguida, precisamos ajustar o pai para aqueles cujo tw do pa é maior que o ramo a ser removido.
fonte
Supondo que você saiba que os elementos raiz são zero, eis o pseudocódigo para saída em texto:
fonte
Você pode emular qualquer outra estrutura de dados com um hashmap, para que essa não seja uma limitação terrível. Digitalizando de cima para baixo, você cria um mapa de hash para cada linha do banco de dados, com uma entrada para cada coluna. Adicione cada um desses hashmaps a um hashmap "mestre", digitado no ID. Se algum nó tiver um "pai" que você ainda não viu, crie uma entrada de espaço reservado para ele no hashmap mestre e preencha-o quando vir o nó real.
Para imprimi-lo, faça uma simples passagem pela profundidade dos dados, acompanhando o nível do recuo ao longo do caminho. Você pode facilitar isso mantendo uma entrada "filhos" para cada linha e preenchendo-a à medida que você verifica os dados.
Quanto à existência de uma maneira "melhor" de armazenar uma árvore em um banco de dados, isso depende de como você usará os dados. Vi sistemas que possuíam uma profundidade máxima conhecida que usava uma tabela diferente para cada nível na hierarquia. Isso faz muito sentido se os níveis na árvore não forem muito equivalentes, afinal (as categorias de nível superior são diferentes das folhas).
fonte
Se mapas de hash ou matrizes aninhados puderem ser criados, eu posso simplesmente descer a tabela desde o início e adicionar cada item à matriz aninhada. Devo rastrear cada linha para o nó raiz para saber em que nível da matriz aninhada inserir. Posso empregar memorização para não precisar procurar o mesmo pai repetidamente.
Editar: eu leria a tabela inteira em uma matriz primeiro, para que não consultasse o banco de dados repetidamente. Claro que isso não será prático se sua mesa for muito grande.
Depois que a estrutura é construída, preciso fazer uma profunda profundidade através dela e imprimir o HTML.
Não há melhor maneira fundamental de armazenar essas informações usando uma tabela (embora eu possa estar errado;) e adoraria ver uma solução melhor. No entanto, se você criar um esquema para empregar tabelas db criadas dinamicamente, abrirá um mundo totalmente novo com o sacrifício da simplicidade e o risco do inferno do SQL;).
fonte
Se os elementos estiverem em ordem de árvore, como mostrado no seu exemplo, você pode usar algo como o seguinte exemplo de Python:
O que isso faz é manter uma pilha representando a posição atual na árvore. Para cada elemento da tabela, ele exibe os elementos da pilha (fechando as divs correspondentes) até encontrar o pai do item atual. Em seguida, ele gera o início desse nó e o envia para a pilha.
Se você deseja gerar a árvore usando recuos, em vez de elementos aninhados, basta pular as instruções print para imprimir as divs e imprimir um número de espaços iguais a alguns múltiplos do tamanho da pilha antes de cada item. Por exemplo, em Python:
Você também pode usar esse método facilmente para construir um conjunto de listas ou dicionários aninhados.
Edit: Vejo pelo seu esclarecimento que os nomes não pretendiam ser caminhos de nós. Isso sugere uma abordagem alternativa:
Isso constrói uma árvore de matrizes de tuplas (!). idx [0] representa a (s) raiz (s) da árvore. Cada elemento de uma matriz é uma tupla de 2 tuplas, que consiste no próprio nó e em uma lista de todos os seus filhos. Uma vez construído, você pode manter o idx [0] e descartar o idx, a menos que queira acessar os nós por seu ID.
fonte
Para estender a solução SQL de Bill, você pode basicamente fazer o mesmo usando uma matriz plana. Além disso, se todas as suas strings tiverem o mesmo comprimento e seu número máximo de filhos for conhecido (digamos em uma árvore binária), você poderá fazer isso usando uma única string (matriz de caracteres). Se você tem um número arbitrário de filhos, isso complica um pouco as coisas ... eu precisaria verificar minhas anotações antigas para ver o que pode ser feito.
Então, sacrificando um pouco de memória, especialmente se sua árvore for escassa e / ou desequilibrada, você pode, com um pouco de matemática de índice, acessar todas as seqüências aleatoriamente armazenando sua árvore, largura primeiro na matriz da seguinte maneira (para um binário árvore):
você sabe o comprimento da corda, você sabe
Estou no trabalho agora, então não posso gastar muito tempo nisso, mas com interesse posso buscar um pouco de código para fazer isso.
Costumávamos fazer isso para pesquisar em árvores binárias feitas de códons de DNA, um processo construiu a árvore, depois a aplainamos para pesquisar padrões de texto e, quando encontrados, embora a matemática do índice (inversão de cima) recupere o nó ... muito rápido e eficiente, resistente, nossa árvore raramente tinha nós vazios, mas poderíamos coletar gigabytes de dados em um instante.
fonte
Pense em usar ferramentas nosql como neo4j para estruturas hierárquicas. por exemplo, um aplicativo em rede como o linkedin usa couchbase (outra solução nosql)
Mas use o nosql apenas para consultas no nível do data mart e não para armazenar / manter transações
fonte