Załóżmy, że masz płaską tabelę, która przechowuje uporządkowaną hierarchię drzew:
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
Oto schemat, na którym mamy [id] Name
. Węzeł główny 0 jest fikcyjny.
[0] ROOT / \ [1] Węzeł 1 [3] Węzeł 2 / \ \ [2] Węzeł 1.1 [6] Węzeł 1.2 [5] Węzeł 2.1 / [4] Węzeł 1.1.1
Jakiego minimalistycznego podejścia zastosowałbyś do wyprowadzenia go do HTML (lub tekstu, jeśli o to chodzi) jako poprawnie uporządkowane, poprawnie wcięte drzewo?
Załóżmy, że masz tylko podstawowe struktury danych (tablice i mapy skrótów), żadnych wymyślnych obiektów z odniesieniami do rodziców / dzieci, bez ORM, bez frameworka, tylko dwie ręce. Tabela jest reprezentowana jako zestaw wyników, do którego można uzyskać dostęp losowo.
Pseudokod lub zwykły angielski jest w porządku, jest to pytanie czysto koncepcyjne.
Pytanie dodatkowe: Czy istnieje zasadniczo lepszy sposób przechowywania takiej struktury drzewa w RDBMS?
EDYCJE I DODATKI
Aby odpowiedzieć na pytanie jednego komentatora ( Marka Besseya ): Węzeł główny nie jest konieczny, ponieważ i tak nigdy nie zostanie wyświetlony. ParentId = 0 to konwencja wyrażająca „to najwyższy poziom”. Kolumna Kolejność określa sposób sortowania węzłów z tym samym rodzicem.
„Zestaw wyników”, o którym mówiłem, można przedstawić jako tablicę skrótów (aby pozostać w tej terminologii). Bo mój przykład miał już tam być. Niektóre odpowiedzi idą o krok dalej i najpierw je konstruują, ale nie ma sprawy.
Drzewo może być dowolnie głębokie. Każdy węzeł może mieć N dzieci. Nie miałem jednak dokładnie na myśli drzewa „milionów wpisów”.
Nie pomyl mojego wyboru nazewnictwa węzłów („Węzeł 1.1.1”) z czymś, na czym można polegać. Węzły można równie dobrze nazwać „Frank” lub „Bob”, nie sugeruje się żadnej struktury nazewnictwa, tylko po to, aby była czytelna.
Opublikowałem własne rozwiązanie, abyście mogli je rozłożyć na kawałki.
Odpowiedzi:
Teraz, gdy MySQL 8.0 obsługuje zapytania rekurencyjne , możemy powiedzieć, że wszystkie popularne bazy danych SQL obsługują zapytania rekurencyjne w standardowej składni.
Testowałem zapytania rekurencyjne w MySQL 8.0 w mojej prezentacji Recursive Query Throwdown w 2017 roku.
Poniżej moja oryginalna odpowiedź z 2008 roku:
Istnieje kilka sposobów przechowywania danych o strukturze drzewa w relacyjnej bazie danych. To, co pokazano w przykładzie, wykorzystuje dwie metody:
Inne rozwiązanie nazywa się zestawami zagnieżdżonymi i można je również przechowywać w tej samej tabeli. Przeczytaj „ Drzewa i hierarchie w SQL for Smarties ” Joe Celko, aby uzyskać więcej informacji na temat tych projektów.
Zwykle wolę projekt o nazwie Tabela zamknięcia (aka „Adjacency Relation”) do przechowywania danych o strukturze drzewa. Wymaga kolejnej tabeli, ale wyszukiwanie drzew jest dość łatwe.
Omawiam tabelę zamknięcia w mojej prezentacji Modele danych hierarchicznych z SQL i PHP oraz w mojej książce Antypatterny SQL: unikanie pułapek programowania baz danych .
Przechowuj wszystkie ścieżki w tabeli zamknięcia, gdzie istnieje bezpośrednie pochodzenie od jednego węzła do drugiego. Dołącz wiersz dla każdego węzła, aby sam się do niego odwoływał. Na przykład używając zestawu danych pokazanego w pytaniu:
Teraz możesz uzyskać drzewo zaczynające się od węzła 1 w następujący sposób:
Dane wyjściowe (w kliencie MySQL) wyglądają następująco:
Innymi słowy, węzły 3 i 5 są wykluczone, ponieważ są częścią oddzielnej hierarchii, nie schodząc z węzła 1.
Re: komentarz e-satis na temat bezpośrednich dzieci (lub bezpośredniego rodzica). Możesz dodać
path_length
kolumnę „ ”ClosureTable
do, aby ułatwić zapytanie dotyczące bezpośredniego dziecka lub rodzica (lub dowolnej innej odległości).Następnie możesz dodać termin do wyszukiwania w celu zapytania o bezpośrednie elementy potomne danego węzła. Są to potomkowie, których
path_length
jest 1.Ponownie skomentuj @ashraf: „A co z sortowaniem całego drzewa [według nazwy]?”
Oto przykładowe zapytanie, aby zwrócić wszystkie węzły potomków węzła 1, dołączyć je do FlatTable, który zawiera inne atrybuty węzła, takie jak
name
, i posortować według nazwy.Ponownie skomentuj @Nate:
Użytkownik zaproponował dziś edycję. Moderatorzy SO zaakceptowali zmianę, ale ja ją cofam.
Edycja sugerowała, że ORDER BY w ostatnim zapytaniu powyżej powinien być
ORDER BY b.path_length, f.name
, prawdopodobnie, aby upewnić się, że kolejność jest zgodna z hierarchią. Ale to nie działa, ponieważ porządkuje „Node 1.1.1” po „Node 1.2”.Jeśli chcesz, aby porządkowanie pasowało do hierarchii w rozsądny sposób, jest to możliwe, ale nie tylko poprzez uporządkowanie według długości ścieżki. Na przykład zobacz moją odpowiedź na hierarchiczną bazę danych MySQL Closure Table - Jak wyciągać informacje we właściwej kolejności .
źródło
parent_id
) ma tylko jeden wiersz do wyrażenia każdej relacji rodzic-dziecko, ale Tabela zamknięcia ma wiele.Jeśli używasz zestawów zagnieżdżonych (czasami nazywanych zmodyfikowanym przejściem drzewa w przedsprzedaży), możesz wyodrębnić całą strukturę drzewa lub dowolne poddrzewo w jej kolejności w drzewie za pomocą jednego zapytania, kosztem wstawek jest droższy, ponieważ musisz zarządzaj kolumnami opisującymi uporządkowaną ścieżkę przez strukturę drzewa.
W przypadku django-mptt użyłem takiej struktury:
Który opisuje drzewo, które wygląda tak (z
id
reprezentowaniem każdego elementu):Lub jako diagram zestawu zagnieżdżonego, co czyni bardziej oczywistym sposób działania wartości
lft
irght
:Jak widać, aby uzyskać całe poddrzewo dla danego węzła, w kolejności drzewa, wystarczy wybrać wszystkie wiersze, które mają
lft
irght
wartości między nimlft
arght
wartościami. Łatwo jest również pobrać drzewo przodków dla danego węzła.level
Kolumna jest trochę denormalizacja dla wygody bardziej niż cokolwiek innego, atree_id
kolumna pozwala na ponowne uruchomienielft
irght
numeracji dla każdego węzła najwyższego poziomu, co zmniejsza liczbę kolumn dotkniętych wkładki, ruchów i delecji, jaklft
irght
kolumny mają być odpowiednio korygowane, gdy operacje te mają miejsce w celu utworzenia lub zamknięcia luk. Zrobiłem kilka notatek programistycznych w czasie, gdy próbowałem owinąć głowę zapytaniami wymaganymi dla każdej operacji.Jeśli chodzi o faktyczną pracę z tymi danymi w celu wyświetlenia drzewa, stworzyłem funkcję
tree_item_iterator
narzędzia, która dla każdego węzła powinna dostarczyć informacji wystarczających do wygenerowania dowolnego rodzaju wyświetlania.Więcej informacji o MPTT:
źródło
lft
irght
dla nazw kolumn, to znaczy ile znaków nie musieliśmy wpisywać? jeden?!To dość stare pytanie, ale ponieważ ma wiele poglądów, myślę, że warto przedstawić alternatywne i moim zdaniem bardzo eleganckie rozwiązanie.
Aby odczytać strukturę drzewa, możesz użyć rekurencyjnych wspólnych wyrażeń tabelowych (CTE). Daje to możliwość pobrania całej struktury drzewa na raz, uzyskania informacji o poziomie węzła, jego węźle nadrzędnym i kolejności w elementach potomnych węzła nadrzędnego.
Pokażę ci, jak to będzie działać w PostgreSQL 9.1.
Utwórz strukturę
Napisz zapytanie
Oto wyniki:
Węzły drzew są uporządkowane według poziomu głębokości. W końcowym wyniku przedstawilibyśmy je w kolejnych wierszach.
Dla każdego poziomu są one uporządkowane według parent_id i node_order w obrębie rodzica. Dzięki temu dowiemy się, jak zaprezentować je w węźle wyjściowym - link do elementu nadrzędnego w tej kolejności.
Mając taką strukturę, nie byłoby trudno stworzyć naprawdę ładną prezentację w HTML.
Rekurencyjne CTE są dostępne w PostgreSQL, IBM DB2, MS SQL Server i Oracle .
Jeśli chcesz przeczytać więcej na temat rekurencyjnych zapytań SQL, możesz sprawdzić dokumentację swojego ulubionego DBMS lub przeczytać dwa moje artykuły dotyczące tego tematu:
źródło
Począwszy od Oracle 9i, możesz używać CONNECT BY.
Począwszy od SQL Server 2005, można używać rekurencyjnego wspólnego wyrażenia tabelowego (CTE).
Oba wygenerują następujące wyniki.
źródło
Odpowiedź Billa jest cholernie dobra, ta odpowiedź dodaje do niej pewne rzeczy, co sprawia, że życzę SO obsługiwanych wątków.
W każdym razie chciałem wesprzeć strukturę drzewa i właściwość Order. W każdym wywołanym węźle zawarłem jedną właściwość,
leftSibling
która robi to samo, coOrder
w pierwotnym pytaniu (zachowaj porządek od lewej do prawej).Więcej szczegółów i kod SQL na moim blogu .
Dzięki Bill Twoja odpowiedź była pomocna w rozpoczęciu!
źródło
Biorąc pod uwagę wybór, używałbym przedmiotów. Tworzę obiekt dla każdego rekordu, w którym każdy obiekt ma
children
kolekcję, i przechowuję je wszystkie w tablicy assoc (/ hashtable), w której kluczem jest identyfikator. I raz przejrzyj kolekcję, dodając dzieci do odpowiednich pól dla dzieci. Prosty.Ale ponieważ nie sprawia ci to przyjemności, ograniczając korzystanie z dobrego OOP, prawdopodobnie wykonałbym iterację w oparciu o:
Edycja: jest podobny do kilku innych pozycji, ale myślę, że jest nieco czystszy. Dodam jeszcze jedną rzecz: jest to bardzo wymagające SQL. To przykre . Jeśli masz wybór, wybierz trasę OOP.
źródło
Zostało to napisane szybko i nie jest ani ładne, ani wydajne (a ponadto bardzo się autoboxuje, konwersja między
int
iInteger
jest denerwujące!), Ale działa.Prawdopodobnie łamie to zasady, ponieważ tworzę własne obiekty, ale hej robię to jako odwrócenie uwagi od prawdziwej pracy :)
Zakłada się również, że wynik / tabela wyników jest całkowicie wczytywana w jakąś strukturę przed rozpoczęciem budowania Węzłów, co nie byłoby najlepszym rozwiązaniem, jeśli masz setki tysięcy wierszy.
źródło
Istnieją naprawdę dobre rozwiązania, które wykorzystują wewnętrzną reprezentację btree indeksów sql. Jest to oparte na świetnych badaniach z 1998 roku.
Oto przykładowa tabela (w mysql).
Jedynymi polami niezbędnymi do przedstawienia drzewa są:
Oto przykładowa populacja 24 węzłów, uporządkowana według tw:
Każdy wynik drzewa można wykonać nierekurencyjnie. Na przykład, aby uzyskać listę przodków węzła o tw = '22 '
Przodkowie
Rodzeństwo i dzieci są trywialne - wystarczy użyć pa pa uporządkowania przez tw.
Potomków
Na przykład zbiór (gałąź) węzłów zrootowanych na tw = 17.
Dodatkowe uwagi
Metodologia ta jest niezwykle przydatna, gdy liczba odczytów jest znacznie większa niż w przypadku wstawek lub aktualizacji.
Ponieważ wstawienie, przesunięcie lub aktualizacja węzła w drzewie wymaga dostosowania drzewa, konieczne jest zablokowanie tabeli przed rozpoczęciem akcji.
Koszt wstawienia / usunięcia jest wysoki, ponieważ wartości indeksu tw i sz (rozmiar gałęzi) będą musiały zostać zaktualizowane we wszystkich węzłach po punkcie wstawienia i odpowiednio dla wszystkich przodków.
Przenoszenie gałęzi polega na przesunięciu wartości tw gałęzi poza zakres, dlatego konieczne jest również wyłączenie ograniczeń klucza obcego podczas przenoszenia gałęzi. Aby przenieść gałąź, wymagane są zasadniczo cztery zapytania:
Dostosuj zapytania o drzewa
Otwieranie / zamykanie luk w drzewie jest ważną podfunkcją używaną przez metody create / update / delete, dlatego dołączam ją tutaj.
Potrzebujemy dwóch parametrów - flagi reprezentującej to, czy zmniejszamy czy powiększamy, oraz indeksu tw węzła. Na przykład tw = 18 (która ma rozmiar gałęzi 5). Załóżmy, że zmniejszamy rozmiar (usuwamy tw) - oznacza to, że używamy „-” zamiast „+” w aktualizacjach poniższego przykładu.
Najpierw używamy (nieco zmienionej) funkcji przodka, aby zaktualizować wartość sz.
Następnie musimy dostosować tw dla tych, których tw jest wyższy niż gałąź do usunięcia.
Następnie musimy dostosować rodzica dla tych, których pa tw jest wyższe niż gałąź do usunięcia.
źródło
Zakładając, że wiesz, że elementy główne są równe zero, oto pseudokod do wyprowadzenia na tekst:
źródło
Możesz emulować dowolną inną strukturę danych za pomocą mapy skrótów, więc nie jest to straszne ograniczenie. Skanując od góry do dołu, tworzysz mapę dla każdego wiersza bazy danych z wpisem dla każdej kolumny. Dodaj każdy z tych skrótów do „głównego” skrótu, wpisanego w id. Jeśli jakikolwiek węzeł ma „element nadrzędny”, którego jeszcze nie widziałeś, utwórz dla niego pozycję zastępczą w głównej mapie skrótów i wypełnij go, gdy zobaczysz rzeczywisty węzeł.
Aby go wydrukować, wykonaj proste przejście przez głębokość, najpierw przechodząc przez dane, śledząc poziom wcięcia po drodze. Możesz to ułatwić, przechowując pozycję „potomną” dla każdego wiersza i wypełniając ją podczas skanowania danych.
To, czy istnieje „lepszy” sposób przechowywania drzewa w bazie danych, zależy od tego, jak zamierzasz korzystać z danych. Widziałem systemy o znanej maksymalnej głębokości, które używały innej tabeli dla każdego poziomu w hierarchii. Ma to sens, jeśli poziomy w drzewie nie są przecież równoważne (kategorie najwyższego poziomu różnią się od liści).
źródło
Jeśli można utworzyć zagnieżdżone mapy skrótów lub tablice, mogę po prostu zejść od początku tabeli i dodać każdy element do zagnieżdżonej tablicy. Muszę prześledzić każdą linię do węzła głównego, aby dowiedzieć się, do którego poziomu w zagnieżdżonej tablicy należy wstawić. Mogę zastosować zapamiętywanie, aby nie musiałem od nowa szukać tego samego rodzica.
Edycja: Najpierw wczytałbym całą tabelę do tablicy, aby nie pytała DB wielokrotnie. Oczywiście nie będzie to praktyczne, jeśli twój stół jest bardzo duży.
Po zbudowaniu struktury muszę najpierw przejść przez nią głębokość i wydrukować HTML.
Nie ma lepszego fundamentalnego sposobu na przechowywanie tych informacji przy użyciu jednej tabeli (choć mogę się mylić;) i chciałbym znaleźć lepsze rozwiązanie). Jeśli jednak stworzysz schemat, w którym zostaną zastosowane dynamicznie tworzone tabele db, otworzysz zupełnie nowy świat, poświęcając się prostocie i ryzyku piekła SQL;).
źródło
Jeśli elementy są uporządkowane w drzewie, jak pokazano w przykładzie, możesz użyć czegoś takiego jak następujący przykład w języku Python:
Pozwala to zachować stos reprezentujący bieżącą pozycję w drzewie. Dla każdego elementu w tabeli wyskakuje elementy stosu (zamykając pasujące elementy div), dopóki nie znajdzie elementu nadrzędnego bieżącego elementu. Następnie wysyła początek tego węzła i wypycha go na stos.
Jeśli chcesz wyprowadzić drzewo za pomocą wcięcia zamiast elementów zagnieżdżonych, możesz po prostu pominąć instrukcje print, aby wydrukować div, i wydrukować liczbę spacji równą wielokrotności wielkości stosu przed każdym elementem. Na przykład w Pythonie:
Można również łatwo użyć tej metody do zbudowania zestawu zagnieżdżonych list lub słowników.
Edycja: z twojego wyjaśnienia wynika, że nazwy nie miały być ścieżkami do węzłów. To sugeruje alternatywne podejście:
To buduje drzewo tablic krotek (!). idx [0] reprezentuje katalog główny drzewa. Każdy element w tablicy to 2-krotka składająca się z samego węzła i listy wszystkich jego elementów podrzędnych. Po zbudowaniu możesz zatrzymać idx [0] i odrzucić idx, chyba że chcesz uzyskać dostęp do węzłów według ich ID.
źródło
Aby rozszerzyć rozwiązanie SQL Billa, możesz zrobić to samo za pomocą płaskiej tablicy. Co więcej, jeśli wszystkie łańcuchy mają tę samą długość, a maksymalna liczba dzieci jest znana (powiedzmy w drzewie binarnym), możesz to zrobić za pomocą jednego łańcucha (tablica znaków). Jeśli masz dowolną liczbę dzieci, to trochę to komplikuje ... Musiałbym sprawdzić moje stare notatki, aby zobaczyć, co można zrobić.
Następnie, poświęcając trochę pamięci, szczególnie jeśli twoje drzewo jest rzadkie i / lub niezrównoważone, możesz, przy odrobinie indeksu, uzyskać dostęp do wszystkich ciągów losowo, przechowując swoje drzewo, szerokość najpierw w tablicy w ten sposób (dla pliku binarnego drzewo):
znasz swoją długość struny, znasz ją
Jestem teraz w pracy, więc nie mogę spędzać dużo czasu, ale z zainteresowaniem mogę pobrać trochę kodu, aby to zrobić.
Robimy to, aby przeszukiwać drzewa binarne zbudowane z kodonów DNA, proces zbudował drzewo, a następnie spłaszcziliśmy je, aby wyszukać wzorce tekstowe, a gdy je znajdziemy, choć matematyka indeksu (odwraca się od góry) odzyskujemy węzeł z powrotem ... bardzo szybkie i wydajne, twarde nasze drzewo rzadko miało puste węzły, ale mogliśmy przeszukać gigabajty danych w mgnieniu oka.
źródło
Pomyśl o użyciu narzędzi nosql, takich jak neo4j, do struktur hierarchicznych. np. aplikacja sieciowa, taka jak linkedin, korzysta z couchbase (inne rozwiązanie nosql)
Ale używaj nosql tylko do zapytań na poziomie danych, a nie do przechowywania / utrzymywania transakcji
źródło