Microsoft SQL Server 2008

HierarchyID – czyli drzewa po nowemu Udostępnij na: Facebook

Opublikowano: 21 lipca 2009
Autor: Marcin Goł

Parent/child – słów kilka  Parent/child – słów kilka
Słów kilka o typie danym jako takim…  Słów kilka o typie danym jako takim…
Efektywne przeglądanie drzewa  Efektywne przeglądanie drzewa
Strategia #1 – depth-first (DF)  Strategia #1 – depth-first (DF)
Strategia #2 - breadth-first (BF)  Strategia #2 - breadth-first (BF)
Czego nie umie HierarchyID (i czy aby na pewno)?  Czego nie umie HierarchyID (i czy aby na pewno)?
Na zakończenie  Na zakończenie

 

Każdy z nas spotkał się z problemem zamodelowania struktury drzewiastej – czy chodziło o strukturę organizacyjną przedsiębiorstwa czy o drzewo genealogiczne – nie ma to znaczenia, problem jest ten sam: potrzeba zamodelowania drzewa z możliwością wstawiania, przesuwania węzłów oraz przeszukiwania drzewa z jak najlepszą wydajnością .

Jak do tej pory, hierarchiczne struktury najczęściej budowano w oparciu o 2 kolumny, w których jedna kolumna przedstawiała ID węzła, a druga przechowywała identyfikator węzła nadrzędnego. Innym, rzadziej stosowanymi podejściami było tworzenie własnych typów danych (w SQL lub .NET CLR) czy budowa łańcucha znaków przechowującego pełną ścieżkę do korzenia, co jak się przekonamy— zostało również po części zaadoptowane w SQL Server 2008.

Do dalszych rozważań o sposobach operowania na drzewach użyjemy tylko klasycznego, określanego jako parent/child oraz nowego opartego o typ danych HierarchyID – dostępny w SQL Server 2008.

Pobierz
Pobierz artykuł w pliku:

Parent/child – słów kilka

Idea nie jest nowa i zapewne wielu osobom znana poprzednich edycji SQL Server. Należy pamiętać, że przed wersją 2005, kiedy nie było możliwości budowania wyrażeń CTE, operacje na strukturach drzewiastych były trudne i mało wydajne. Po pojawieniu się wspomnianej wersji serwera baz danych sytuacja uległa znaczącej zmianie.

Poniżej kilka przykładowych podstawowych zastosowań:

use tempdb

go

if object_id('dbo.structure') is not null

    drop table dbo.structure

if object_id('dbo.nodes') is not null

    drop table dbo.nodes

go

create table dbo.nodes (id int primary key, name varchar(50))

create table dbo.structure (child int, parent int

constraint str_child_id foreign key (child) references dbo.nodes(id),

constraint str_parent_id foreign key (parent) references dbo.nodes(id)

)

go



-- standardowa struktura drzewiasta zamodelowana przy pomocy parent/child

insert into dbo.nodes (id,name) 

values (1,'pierwszy'),

    (2,'drugi'),

    (3,'trzeci'),

    (4,'czwarty'),

    (5,'piaty'),

    (6,'szosty'),

    (7,'siodmy'),

    (8,'osmy'),

    (9,'dziewiaty')

insert into dbo.structure (child, parent)

values (1,null),

    (2,1),(3,1),

    (4,2),(5,2),(6,3),(7,3),

    (8,4),(9,4)



select t1.child, t2.name, t1.parent, t3.name 

from dbo.structure t1 

join dbo.nodes t2 on (t1.child = t2.id)

join dbo.nodes t3 on (t1.parent = t3.id) 



-- rekurencyjne cte tworzace strukture drzewa

-- dodatkowo mozna uzyc row_number() z partition by/order by celem budowy

-- drzewa w zwracanym secie

;with struct(parent, child, lvl) as

(

    select parent, child, 0 as lvl

    from dbo.structure 

    where parent is null

    union all

    select i2.parent, i2.child, i1.lvl + 1 from struct i1

    join dbo.structure i2 on (i1.child = i2.parent)

)

select x1.child as NodeId, x2.name as NodeName,x1.parent as ParentNode,x1.lvl as Level

from struct x1 join dbo.nodes x2 

on (x1.child = x2.id)

order by Level,ParentNode



-- rekurencyjne cte budujace sciezke od wskazanego wezla do korzenia

-- zwracana w dosc ciekawy sposob (przy pomocy for xml path(''))

;with struct(parent, child, lvl) as

(

    select parent, child, 0 as lvl

    from dbo.structure 

    where child = 9

    union all

    select i2.parent, i2.child, i1.lvl + 1 from struct i1

    join dbo.structure i2 on (i1.parent = i2.child)

)

select (select '/' + cast(x1.child as varchar(20)) + ';' + x2.name  

from struct x1 join dbo.nodes x2 

on (x1.child = x2.id) 

order by x1.lvl desc

for xml path('')) as SciezkaDoRoot



-- przepiecie poddrzewa

update dbo.structure set parent = 7 where child = 6

Jak widać na przykładzie powyższego kodu wyrażenia CTE wprowadzone w SQL Server 2005, spowodowały uproszczenie zapytań przeglądających struktury parent/child.

Owszem, przy dużych drzewach rekurencja może powodować znaczące obciążenie – od czego są indeksy ? Czego więc brakowało ? Co takiego dano nam w nowym typie danych HierarchyID, czego nie mieliśmy w poprzednim podejściu? Jakie są koszty korzystania z HierarchyID ?

 Do początku strony Do początku strony

Słów kilka o typie danym jako takim…

HierarchyID (HID) jest typem danych o zmiennej długości, fizycznie będacy w SQL Server biblioteką CLR: Microsoft.SqlServer.Type.

HID charakteryzuje się wysokim stopniem kompresji – dla przykładu przechowanie 100.000 węzłów o stopniu rozgałęzienia drzewa równym 6 (każdy węzeł ma 6 dzieci) – zajmuje 38bitów, co po zaokrągleniu do pełnych bajtów daje nam 40 bitów=5bajtów.

Należy zwrócić uwagę, że typ danych ma limit długości wartości równy 892 bajtów, więc jeśli drzewo potrzebuje przechować informacje wykraczające poza ten limit, to operacja taka się nie powiedzie. Niestety ze względu na specyfikę HID trudno powiedzieć ile węzłów będzie zawierało drzewo, które nie zmieści się w w limicie 892bajtów, ale można wykonać prosty testy:

Budowa drzewa w głąb

if (object_id('dbo.hidtest') is not null)

    drop table dbo.hidtest

create table dbo.hidtest ( n HierarchyID)

go

insert into dbo.hidtest (n) values (HierarchyID::GetRoot())



declare @hid HierarchyID

set @hid = (HierarchyID::GetRoot()).GetDescendant(null,null)



declare @i int

set @i = 0

while @i > -1

begin



    begin try

    

        insert into hidtest (n) values (@hid)

        set @hid = @hid.GetDescendant(null,null)

        set @i = @i + 1



    end try

    

    begin catch



        declare @emes nvarchar(2048),@esev int,@esta int, @enum int   

        set @emes = ERROR_MESSAGE()   

        set @esev = ERROR_SEVERITY()   

        set @esta = ERROR_STATE()   

        set @enum = ERROR_NUMBER()    

        raiserror(@emes,@esev,@esta,@enum)   



        print 'Wartosc HID: ' + @hid.ToString()

        print 'Poziom HID: ' + cast (@hid.GetLevel() as varchar(10))

        set @i = -1

    end catch

end

Powyższy skrypt zbudował u mnie drzewo mające 1428 poziomów (wliczając korzeń), dopiero przy próbie dodania kolejnego otrzymałem błąd:

Msg 6522, Level 16, State 2, Line 21

A .NET Framework error occurred during execution of user-defined routine or aggregate "HierarchyID": 

Microsoft.SqlServer.Types.HierarchyIDException: 24006: SqlHierarchyID.GetDescendant failed because its result is too big.

Microsoft.SqlServer.Types.HierarchyIDException: 

   at Microsoft.SqlServer.Types.OrdPath.AppendBits(UInt16& bitOffset, UInt64 x, UInt16 nBits)

   at Microsoft.SqlServer.Types.OrdPath.WriteOrd(UInt16& bitOffset, Int64 ord)

   at Microsoft.SqlServer.Types.XmlIdGenerator.GetNextID()

   at Microsoft.SqlServer.Types.SqlHierarchyID.GetDescendant(SqlHierarchyID child1, SqlHierarchyID child2)

Poniżej prosty przykład prezentujący uzycie HID:

if object_id ('tempdb.dbo.#t') is not null

    drop table #t

create table #t (node HierarchyID, NodeLevel as node.GetLevel() )

go

declare @root HierarchyID

set @root = HierarchyID::GetRoot()

-- korzystajac z "przewidywalnosci" HierarchyID, uzywam stalych wartosci

-- zeby zaprezentowac, jak rozna postac moga przyjmowac dane

insert into #t (node) values (@root)

insert into #t (node) values (@root.GetDescendant(null,null))

insert into #t (node) values (@root.GetDescendant(null,0x58))

insert into #t (node) values (@root.GetDescendant(null,0x48))

insert into #t (node) values (@root.GetDescendant(0x3F80,0x48))

insert into #t (node) values (@root.GetDescendant(null,0x42C0))

insert into #t (node) values (@root.GetDescendant(null,0x3E80))

insert into #t (node) values (@root.GetDescendant(0x3D80,0x3E80))

insert into #t (node) values (@root.GetDescendant(0x3D80,0x3E2C))

insert into #t (node) values (@root.GetDescendant(0x3D80,0x3E24))

insert into #t (node) values (HierarchyID::Parse('/-3.1/').GetDescendant(null,null))

insert into #t (node) values (HierarchyID::Parse('/-3.1/').GetDescendant(0x3E2D60,null))

insert into #t (node) values (HierarchyID::Parse('/-3.1/').GetDescendant(0x3E2DA0,null))



select node as Node, node.ToString() as NodeString, NodeLevel 

from #t

order by node

Zapytanie z ostatnich lini polecenia zwraca nam następujący wynik:

Zróżnicowanie wartości HierarchyID ze względu na kolejność i miejsce wstawiania

Rysunek 1: Zróżnicowanie wartości HierarchyID ze względu na kolejność i miejsce wstawiania.

W powyższym przykładnie użyłem kilku metod udostępnionych przez typ danych HID :

  • GetRoot() – metoda zwraca wartość korzenia dla nowego drzewa HID.
  • GetDescendant(child1,child2) – metod zwraca wartość HID dla nowego węzła uwzględniając jako położenie argumenty child1, child2. Child1 powinien mieć mniejszą wartość niż child2. Jeśli przekażemy tylko child1, a child2 zostanie null, to zostanie zwrócona wartość dla nowego węzła „po” child1 (wartość HierarchyID rośnie),a w przeciwnym wypadku —przed child2 (wartość maleje). Jeśli przekażemy obydwa parametry , to zostanie zwrócona wartość dla potomka pomiędzy tymi węzłami.
  • ToString() – przedstawia hierarchię w postaci łańcucha znaków. Zwracana wartość jest typu nvarchar(4000).
  • GetLevel() – zwraca informację o poziomie zagnieżdżenia w drzewie. Wartość dla korzenia to 0, dla jego potomków kolejne liczby naturalne.

Uwaga ogólna do wszystkich nazw metod — są to metody napisane w .NET CLR, a więc niezależnie od wybranego sposobu segregowania (ang. collation) w SQL Server, należy pamiętać że ma znaczenie, czy piszecie nazwy metod wielkimi czy małymi literami. Próba odwołania się do metody getroot() zamiast GetRoot() – powoduje błąd kompilacji i jest jednym z częstszych błędów początkujących.

Pracując z nowym typem danych HierarchyID należy pamiętać, że:

  • Korzeń (ang. Root) drzewa zawsze ma wartość 0x (po rzutowaniu string ‘/’),
  • HID operuje na liczbach całkowitych,
  • Jeśli brakuje miejsca pomiędzy wartościami, to stosowana jest kropka –np. wstawienie nowego węzła pomiędzy /1 i /2 spowoduje że wartość nowego to /1.1/,
  • Wartość /1.01/ jest błędna – zera porzedzające są zakazane (ponieważ mogły by wystąpić problemy w parsowaniu) poprawna reprezentacja to /1.1/.

 Do początku strony Do początku strony

Efektywne przeglądanie drzewa

Na potrzebny przeszukiwania drzewa przygotowano odpowiedni zestaw procedur oraz dwie typowe strategie indeksowania.

Na początek zajmijmy się procedurami przeszukującymi drzewo:

  • GetAncestor(n) – która pobiera ojca o n poziomów wyżej w hierarchii, dla n = 0 zwracany jest wartość węzła na jakim wywołujemy metodę, dla n = 1 dostaniemy jego ojca itd.
  • IsDescendantOf(parent) – sprawdza, czy węzeł jest potomkiem węzła przekazanego w argumencie.Wywołanie metody dla samego siebie zwraca 1 (typ danych bit).

W takim razie w jaki sposób pobrać wszystkie węzły z danego poddrzewa? Przeanalizujmy poniższy kod:

declare @subtreeRoot HierarchyID

-- wartosc root poddrzewa

set @subtreeRoot = HierarchyID::Parse('/-3.1/') 



select node as Node, node.ToString() as NodeString, NodeLevel

from #t t1

where 

t1.node >= @subtreeRoot

and t1.node < @subtreeRoot.GetAncestor(1).GetDescendant(@subtreeRoot,null)

W powyższym przykładzie widać że typ HID obsługuje m.in. operatory wiekszości (‘>’), równości (‘=’). Ze specyfiki budowy typu danych wynika że poddrzewo ma zawsze większe wartości niż korzeń. Znając te zależność – pobranie poddrzewa wskazanego węzła można sprowadzić do jednego prostego SELECT. Ważnym jest że zapytamy o wartości większe niż badany korzeń oraz o mniejsze niż następny węzeł na tym samym poziome co wskazany root.

Zwrócmy teraz uwagę na wywołanie typowe dla języków obiektowych: GetAncestor(…).GetDescendant(…)).
GetAncestor(1) pobiera bezpośredniego ojca węzła dla którego wywołujemy metodę, a dla znalezionego ojca dzieki procedurze GetDescendant(@subtreeRoot,null) pobieramy wartość potomka następnego po wartości korzenia poszukiwanego poddrzewa. Brzmi to być może skomplikowanie, ale zrozumienie charakteru HID pozwala tworzyć o wiele prostsze i szybsze zapytania.

Istnieje metoda DescendantLimit(), która jest jednak metodą prywatną klasy Microsoft.SqlServer.Types.SqlHierarchyID i w związku z tym nie można jej użyć, a jeśli ktoś spróbuje otrzyma poniższy komunikat:

„Msg 6574, Level 16, State 2, Line 19

Method, property or field 'DescendantLimit' of class 'Microsoft.SqlServer.Types.SqlHierarchyID' in assembly 'Microsoft.SqlServer.Types' is not public.”

Trochę szkoda – gdyż upublicznienie tej metody ułatwiło by nam życie - skracając długie wywołanie zaprezentowane powyżej.

Optymalizację przeszukiwania drzewa uzyskamy tworząc odpowiednią strategię indeksowania:

 Do początku strony Do początku strony

Strategia #1 – depth-first (DF)

Strategia depth-first

Rysunek 2: Strategia depth-first.

Analizując powyższy rysunek można stwierdzić, że stosując strategię DF wiersze należące do poddrzew danego węzła znajdują się bliżej tego węzła niż węzły znajdujące się na tym samym poziomie co badany węzeł.

 Do początku strony Do początku strony

Strategia #2 - breadth-first (BF)

Strategia breadth-first

Rysunek 3: Strategia breadth-first.

W tym przypadku jest to zorganizowane w taki sposób, aby w pobliżu danego węzła znajdowały się wiersze leżące na tym samym poziomie zagnieżdżenia.

Utwórzmy zatem indeksy dla każdej ze strategii i sprawdźmy jak zachowują się w praktyce:

create clustered index temp_Breadth_First 

ON #t(NodeLevel,Node);

GO

create nonclustered index temp_Depth_First 

ON #t(Node);

GO

Przeanalizujmy plany zapytań dla różnych przypadków, dla zapytania pobierającego poddrzewo dla wskazanego węzła czyli:

select node as Node, node.ToString() as NodeString, NodeLevel

from #t t1

where 

t1.node >= @subtreeRoot

and t1.node < @subtreeRoot.GetAncestor(1).GetDescendant(@subtreeRoot,null)

a) Brak indeksów:

Brak indeksów

Rysunek 4: Brak indeksów.

b) Po utworzeniu indeksu klastrowanego – dla strategii BF:

Po utworzeniu indeksu klastrowanego – dla strategii BF

Rysunek 5: Po utworzeniu indeksu klastrowanego – dla strategii BF.

c) Po dodaniu indeks nieklastrowanego dla strategii DF:

Po dodaniu indeks nieklastrowanego dla strategii DF

Rysunek 6: Po dodaniu indeks nieklastrowanego dla strategii DF.

Jeszcze raz przeanalizujmy zapytanie, pytamy o wiersze z wartością HID większą niż wskazany korzeń oraz mniejszymy od następnej wartości na tym poziomie drzewa.

W przypadku A, SQL Server nie miał zbyt dużego wyboru – brak indeksów – musiał wykonać skan tabeli, w kolejnym przypadku kiedy pojawił się indeks – niestety mało przydatny (jego klucz zawierał kolumny: NodeLevel,Node) – w związku z czym konieczny okazał się skan indeksu. W ostatnim przypadku dzięki temu że funkcja GetDescendant() jest deterministyczna – SQL Server mógł spokojnie zastosować przeszukanie indeksu (czyli skorzystał ze strategii DF w celu pobrania poddrzewa) – znacząco przyspieszając wyszukiwanie.

Sprawdźmy, jak będzie wyglądał plan zapytania, jeśli będziemy chcieli pobrać węzły będace dzieckiem (czyli wpasowujące się w strategie BF). W tym celu wykorzystamy nieptymalne na pierwszy rzut oka polecenia:

select node as Node, node.ToString() as NodeString, NodeLevel

from #t where node.GetAncestor(1) = @subtreeRoot

Zapytanie to będzie miało następujący plan:

a) Bez indeksów

Bez indeksów

Rysunek 7: Bez indeksów.

b) Po utworzeniu indeksu zgrupowanego (strategia BF):

Po utworzeniu indeksu zgrupowanego (strategia BF)

Rysunek 8: Po utworzeniu indeksu zgrupowanego (strategia BF).

c) Po dodaniu indeksu niezgrupowanego (strategia DF):

Po dodaniu indeksu niezgrupowanego (strategia DF)

Rysunek 9: Po dodaniu indeksu niezgrupowanego (strategia DF).

Przypadek A nas niczym nie zaskakuje – brak indeksów – skan tabeli, jednakże patrząc jeszcze raz na zapytanie zauważamy że wywołujemy metodę GetAncestor() na każdym węźle, w związku z tym czy nie będziemy zawsze skanować tabeli/indeksu?. Odpowiedź na to pytanie znajdziemy przeglądając plan zapytania w formie tekstowej, okazuje się że zapytanie jest przekształcane w następujący sposób:

select node as Node, node.ToString() as NodeString, NodeLevel

from #t where nodelevel = @subtreeRoot.GetLevel()+1

and node >= @subtreeRoot

and node <= @subtreeRoot.DescendantLimit()



-- a jako ze wiemy iz DescendantLimit jest metoda prywatna to mozemy

-- zapytanie napisac po swojemu:



select node as Node, node.ToString() as NodeString, NodeLevel

from #t where nodelevel = @subtreeRoot.GetLevel()+1

and node >= @subtreeRoot

and node <= @subtreeRoot.GetAncestor(1).GetDescendant(@subtreeRoot,null)

Co powoduje że zamiast przeskanowania wszystkich wierszy w tabeli wykorzystywany indeks z wyliczalną kolumną NodeLevel. Ponieważ indeks miał klucz w postaci NodeLevel, Node – powyższe zapytanie idealnie się w niego wpasowywuje.

Podobne przekształcenia są stosowna dla innych metod klasy HierarchyID – można o tym przeczytać tutaj.

 Do początku strony Do początku strony

Czego nie umie HierarchyID (i czy aby na pewno)?

Przede wszystkim typ danych HierarchyID nie zarządza poprawnością tworzonych węzłów, nie sprawdza czy są luki w numeracji lub czy nie jest dodawany istniejący już węzeł. Część z tych „braków” możemy ominąć stosując indeksy czy instrukcję CHECK. Są jednak operacje, które wykonać trudniej, np. przenoszenie węzłów czy migracja ze struktury parent/child.

Zacznijmy w takim razie od problemu opisanego jako ostatni, czyli od migracji ze struktury parent/child do struktury z typem danych HierarchyID:

Dla przypomnienia – będziemy posługiwać się tabelami dbo.nodes oraz dbo.structure utworzonymi na poczatku artykułu:

if object_id('dbo.structure') is not null

    drop table dbo.structure

if object_id('dbo.nodes') is not null

    drop table dbo.nodes

go

create table dbo.nodes (id int primary key, name varchar(50))

create table dbo.structure (child int, parent int

constraint str_child_id foreign key (child) references dbo.nodes(id),

constraint str_parent_id foreign key (parent) references dbo.nodes(id)

)

go



-- standardowa struktura drzewiasta zamodelowana przy pomocy parent/child

insert into dbo.nodes (id,name) 

values (1,'pierwszy'),

    (2,'drugi'),

    (3,'trzeci'),

    (4,'czwarty'),

    (5,'piaty'),

    (6,'szosty'),

    (7,'siodmy'),

    (8,'osmy'),

    (9,'dziewiaty')

insert into dbo.structure (child, parent)

values (1,null),

    (2,1),(3,1),

    (4,2),(5,2),(6,3),(7,3),

    (8,4),(9,4)

Rozwiązując problem migracji możemy zastosować dwa podejścia:

  1. Znając sposób budowy drzewa HID możemy pobierać po jednym węźle, który już ma ojca – jemu przypisywać wartość HID. Operację tę wykonujemy do momentu,w którym nie będzie już węzłów do ponumerowania. Rozwiązanie to choć zgodne z metodą budowy drzew, jest jednak mało efektywne,
  2. Drugi pomysł wykorzystuje specyfikę HierarchyID, czyli przydzielanie kolejnych identyfikatorów dla kolejnych dzieci wybranego ojca. Identyfikatory te są przydzielane znaną z poprzedniej wersji serwera skalarną funkcją ROW_NUMBER().
;with str(parent, child, num) as 

(

select t1.parent, t1.child,

ROW_NUMBER() OVER (PARTITION BY t1.parent ORDER BY t1.child) as num

from dbo.structure t1

),

paths(path, child) as

(

SELECT HierarchyID::GetRoot() AS Node, t2.child 

FROM str t2

WHERE t2.parent is null

UNION ALL 

SELECT 

CAST(t4.path.ToString() + CAST(t3.Num AS varchar(30)) + '/' AS HierarchyID), t3.child

FROM str t3 

JOIN paths t4 

   ON t3.parent = t4.child 

)

SELECT t1.path, t1.path.ToString(), t1.child, t2.name

from paths t1 join dbo.nodes t2 on (t1.child = t2.id)

order by path

Powyższe zapytanie jest poprawioną wersją kodu dostępnego w Books Online (gdzie występują błedy czeskie). Szybka analiza: wyrażenie CTE str(parent, child, num) numeruje wiersze drzewa partycjonując je po węzłach nadrzędnych (czyli tak jak działa HierarchyID). Kolejne paths(path,child) zaczyna przeglądanie drzewa od korzenia i buduje wartości tekstowe HID. Całe zapytanie daje następujący wynik:

Migracja parent/child - HierarchyID w jednym zapytaniu ?

Rysunek 10: Migracja parent/child -> HierarchyID w jednym zapytaniu ?.

Przenoszenie poddrzewa w innej miejsce także okazuje się równie łatwe:

declare @nroot HierarchyID, @oroot HierarchyID



select @oroot = node from #t where node = '/-3.1/'



SELECT @nroot = node from #t where node = '/-3.-1/'



select @nroot = @nroot.GetDescendant(max(node), null) 

from #t where node.GetAncestor(1) = @nroot



select node as 'OldNode', 

node.ToString() as 'OldPath',

node.GetReparentedValue(@oroot, @nroot) AS 'NewNode',

node.GetReparentedValue(@oroot, @nroot).ToString() AS 'NewPath'

from #t

where node.IsDescendantOf(@oroot) = 1

and node.GetLevel() > @oroot.GetLevel()

Przenoszenie poddrzewa – czyli wskazanie że całe poddrzewo węzła @oroot (z tym węzłęm włącznie), będzie przeniesione pod węzeł @nroot. W związku z tym używamy metody GetReparentedValue(), która właśnie pobiera poprzedni i nowy korzeń – a zwraca nową wartość HierarchyID dla węzła, z uwzględnieniem zmiany korzenia.

Po uruchomieniu zapyatnia otrzymujemy nastepujący wynik:

Przenoszenie poddrzewa w HierarchyID

Rysunek 11: Przenoszenie poddrzewa w HierarchyID.

Mając nową ścieżkę wystarczy ją przepisać do wartości HID poddrzewo zostaje przesunięte. Ten sam efekt można osiągnąc przy pomocy funkcji np.: stuff:

stuff(

node.ToString(), -- ze stringa

1, -- zaczynajac od jego pierwszego znaku

len(@oroot.ToString()), -- usun taka liczbe znakow

@nroot.ToString()) -- i zastap je tymi

Proponuję samodzielnie wykonać sprawdzenie tego przypadku.

Można sobie wybrazić inny przypadek przenoszenia poddrzewa, a mianowicie użytkownik podaje poprzedni i nowy korzeń, a procedura przepisuje wszystkie węzły poniżej starego korzenia, bezpośrednio pod nowy. Można to zrealizować modyfikując rozwiązanie podane wyżej, tzn.: należalo by wówczas pobrać węzły będące bezpośrednimi potomkami starego korzenia, i każego tak wycięte poddrzewo przeniesić pod nowy korzeń.

 Do początku strony Do początku strony

Na zakończenie

Przeglądając w Internecie zasoby dotyczące HierarchyID można często spotkać zarzuty, że korzystając z niego należy pamiętać o wielu kruczkach i zależnościach – np. o lukach w numeracji czy odpowiedniej przebudowie drzewa. Moja odpowiedź na takie uwagi jest jedna: HierarchyID to typ danych a nie mechanizm budowy struktur drzewiastych. W związku tym to do aplikacji należy zarządzanie jej wartościami. Ponadto brak mechanizmów automatyzacji umożliwia większą elastyczność i wydajność.

Oczywiście typ HierarchyID ma swoje ograniczenia, ale myślę, że możliwości jakie on daje podczas pracy ze strukturami drzewiastymi, zdecydowanie przechyla szalę wagi na jego stronę. Ponadto w przypadku kiedy limit 892 bajtów byłbym zbyt dużym ograniczeniem można wykonać kilka drzew łączonych mechanizmami własnymi – parent/child.

Z ciekawostek można jeszcze powiedzieć, że kiedy są tworzone indeksy na polach typu XML, to typ danych HID jest wykorzystywany wewnątrz silnika baz danych do reprezentacji, w której pozycji drzewa dokumentu aktualnie się znajdujemy… co dowodzi jak bardzo wydajny i elastyczny jest to mechanizm.


Marcin Goł Marcin Goł
Architekt baz danych, ISCG
Aktualnie pracuje w ISCG, gdzie projektuje i optymalizuje bazy danych; w swojej pracy głównie wykorzystuje technologie Microsoft SQL Server. Wcześniej brał udział w wielu wdrożeniach systemów OSS dla sektora telekomunikacyjnego oraz pracował jako administrator środowiska opartego o technologię Microsoft. Lider Polish SQL Server User Group (PLSSUG); aktywny użytkownik portalu WSS.pl, prowadzi blog SQL Server. Specjalizuje się w analizie i rozwiązywaniu problemów wydajnościowych. W styczniu 2009 został nagrodzony tytułem Most Valuable Professional w kategorii SQL, ponadto posiada certyfikaty firmy Microsoft, min.: MCITP: Database Administrator.
 Do początku strony Do początku strony

Microsoft SQL Server 2008