Optymalizacja Datalogu
Adrian Szyndela
8 listopada 2001
1. Wprowadzenie.
Przypomnienie:
- IDB - intensjonalna baza danych
- EDB - ekstensjonalna baza danych
Kilka pojęć:
- predykaty wbudowane - predykaty o standardowej interpretacji (np. X < Y)
- predykaty zwykłe - predykaty bez standardowej interpretacji
- bezpieczna reguła - reguła, w której każda zmienna z nagłówka występuje także w ciele w zwykłych oraz nie zanegowanych predykatach
W regułach dopuszczamy obecność symboli funkcyjnych (czyli działamy na rozszerzonym Datalogu).
2. Podstawowe algorytmy obliczania zapytań.
Podstawowe algorytmy obliczania zapytań dzielą się na algorytmy wstępujące (ang. bottom-up) oraz algorytmy zstępujące (ang. top-down). Algorytmy wstępujące budują (niejawnie) drzewo dowodu zaczynając od liści (relacje z EDB) i idąc w górę aż do korzenia (danego zapytania). Algorytmy zstępujące zaczynają od zapytania i rozważają cieżki, które mogą doprowadzić do sukcesu - dowodu.
2.1 Algorytmy wstępujące
Najprostszym i od razu narzucającym się algorytmem jest algorytm naiwny:
- Dla każdej reguły dla predykatu p obliczamy relację P używając poprzednio obliczonych relacji oraz relacji z EDB.
- Sumujemy wszystkie relacje dla p - w wyniku otrzymujemy P'.
- Porównujemy P z P'. Jeśli P=P' dla wszystkich predykatów, to znaleźliśmy najmniejszy punkt stały - koniec. Jeśli natomiast jakieś P jest podzbiorem P' to powtarzamy proces obliczając kolejne przybliżenie relacji (przy P zastąpionym przez P').
Łatwo zauważyć, że algorytm ten wykonuje sporo niepotrzebnych obliczeń, gdyż każde następne przybliżenie relacji zawiera poprzednie przybliżenie i ewentualnie coś jeszcze. Mniej obliczeń wykonuje algorytm semi-naiwny:
- dla każdego predykatu p z IDB tworzymy predykat Δp reprezentujący zmianę p w jednym cyklu obliczania (czyli relacja dla Δp jest różnicą P'-P z algorytmu naiwnego).
- zamieniamy regułę:
H <- G1, ..., Gn
na serię reguł różnicowych - po jednej dla każdego predykatu Gi z IDB:
ΔH <- G1, ..., Gi-1, ΔGi, Gi+1, ..., Gn.
- początkowe ΔP inicjalizujemy jednym przebiegiem algorytmu naiwnego, ale wykonując go tylko dla tych reguł, w których ciele nie ma predykatów z IDB.
- jeśli wszystkie ΔP są puste - koniec.
- zamieniamy wszystkie relacje P z IDB przez sumę P z ΔP.
- dla każdego predykatu p z IDB obliczamy nową relację różnicową ΔP' dla każdej z reguł różnicowych (czyli z Δp w nagłówku) używając relacji z EDB, bieżących relacji P, oraz bieżących relacji różnicowych. Następnie sumujemy relacje dla każdego z Δp.
- dla każdej relacji z IDB obliczamy ΔP = ΔP' - P. Wracamy do (1).
Dopuszczając symbole funkcyjne narażamy się na możliwość zapętlenia się algorytmów - w przypadku, gdy najmniejszy punkt stały operatora bezpośredniej konsekwencji jest nieskończony. Bez symboli funkcyjnych bezpieczne reguły przy skończonej bazie danych gwarantują, że powyższe algorytmy zakończą się.
2.2 Algorytmy zstępujące
Podstawowym algorytmem zstępującym jest SLD-rezolucja, która została omówiona na poprzednim referacie.
2.3 Porównanie algorytmów
Obliczanie zapytań ma za zadanie wyprodukować pewną relację bądź część relacji, która jest potrzebna by odpowiedzieć na zapytanie. W przypadkach zapytań, które potrzebują niepełnych relacji SLD-rezolucja działa przeważnie szybciej niż algorytmy naiwne oraz potrzebuje mniej pamięci. Jest też jednak mniej stabilna - może wpaść w nieskończoną pętlę, podczas gdy algorytmy naiwne kończą się sukcesem. Okazuje się, że przy zastosowaniu najlepszych technik w obu przypadkach metody zstępujące nigdy nie są lepsze od wstępujących.
3. Algorytmy ciekawsze niż podstawowe.
Największymi mankamentami SLD-rezolucji jest chyba możliwość zapętlenia się na dobrych programach oraz niepełność wyników obliczeń w takim wypadku. Drzewa reguł/celów to próba stworzenia metody zstępującej, która nie miałaby tych niepożądanych cech, a zarazem posiadała zalety SLD-rezolucji.
3.1 Drzewa reguł/celów.
Drzewa reguł/celów mają dwa rodzaje węzłów: węzły reguł i węzły celów. Drzewo reguł/celów dla danego zapytania G0 konstruuje się następująco:
- Korzeń jest węzłem celu dla zapytania G0.
- Jeśli węzeł n jest węzłem celu dla celu G, oraz G zawiera predykat p, to dla każdej reguły r dla p unifikujemy nagłówek r z G. Jeśli operacja się powiedzie, tworzymy węzeł reguły dla zmodyfikowanej wersji r, która staje się synem n.
- Synami węzła reguły są węzły celów odpowiadające poszczególnym podcelom reguły.
Podstawowy algorytm polega na przechodzeniu drzewa od korzenia do liści (w głąb). Drzewo to nie jest jawnie konstruowane jako struktura danych a jedynie jest wyobrażeniem ścieżki algorytmu, na który składają się dwie wzajemnie rekurencyjne procedury (jedna dla węzłów reguł, druga dla węzłów celów). Algorytm ten jako że jest ideowo niemal identyczny z SLD-rezolucją posiada wszystkie jej wady i zalety. Łatwo poprawić ten algorytm przechodząc drzewo wszerz - jest to wersja z zastosowaniem kolejki (QRGT). Znika wtedy problem niepełności obliczeń. Pojawia się jednak inny problem: nie wiadomo, w którym momencie odpowiedź jest już pełna.
3.2 Grafy reguł/celów.
Grafy reguł/celów służą reprezentacji zbioru klauzul Horna. W porównaniu do drzew reguł/celów nie mają struktury warstwowej oraz posiadają dodatkowe elementy: szablony związania:
- Szablony związania dla węzłów celu: napisy składające się z liter b i f. Jeśli i-tą literą jest b to znaczy, że i-ty argument predykatu jest związany (znamy jakieś podstawienia dla niego). Jeśli natomiast i-tą literą jest f to znaczy to, że i-ty argument jest wolny.
- Szablony związania dla węzłów reguł mówią o tym, które zmienne w regule są związane, a które wolne:
- Zmienna występująca w związanym argumencie nagłówka jest związana przed rozważaniem podcelów.
- Zmienna jest związana po rozważeniu podcelu Gi jeśli była związana przed rozważaniem Gi lub jeśli występuje w Gi.
Fakt, że zmienne Xi są związane a Yi wolne oznaczamy przez [X1,...,Xm|Y1,...,Yn].
Konstrukcja grafu:
- r0 oznacza regułę r przed rozpatrzeniem podcelów, r1 po rozpatrzeniu pierwszego itd...
- węzeł celu z predykatem z EDB nie ma następników;
- węzeł celu z predykatem p z IDB posiadającym szablon związania a ma następniki odpowiadające wszystkim regułom dla p. Jeśli r jest taką regułą, to pa ma następnik:
gdzie X1,...,Xn to wszystkie zmienne występujące w związanych argumentach nagłówka r według szablonu a, a Y1,...,Ym to pozostałe zmienne.
- Rozważmy węzeł reguły ri[X1,...,Xn|Y1,...,Ym], i >= 0. Niech q(t1,...,tk) będzie (i+1)-szym podcelem r.
- Pierwszym następnikiem jest węzeł celu qb. b jest szablonem, który oznacza j-ty argument z q jako związany jeśli wszystkie zmienne w tj są zwišzane (X1,...,Xn). W przeciwnym przypadku j-ty argument jest oznaczony jako wolny.
- Jeśli i+1 jest mniejsze od liczby podcelów w r (czyli q(t1,...,tk) nie jest ostatnim podcelem w r) to następnikiem jest węzeł:
ri+1[X1,...,Xn,U1,...,Uj|V1,...,Vl]
gdzie U1,...,Uj są zmiennymi spośród Y1,...,Ym, które występują w q, a V1,...,Vl są pozostałymi zmiennymi spośród Y1,...,Ym.
3.2.1. Unikalne szablony związania.
Korzystając z grafu reguł/celów łatwo wskazać szablony związania, z którymi poszczególne predykaty są używane. Mając tę informację możemy "rozdzielić" predykaty tak, aby każdy miał unikalny szablon związania:
- Dla każdego szablonu a dla predykatu p (z IDB) takiego, że pa pojawia się w grafie, tworzymy nowy predykat p_a.
- Dla każdej z reguł dla p tworzymy zmodyfikowaną kopię reguły - r_a z p_a w nagłówku.
- Węzeł reguły dla r0 (następnik węzła celu pa) jest początkiem łańcucha węzłów r1,...,rk. Węzły te mają następników - węzły celów odpowiadające podcelom reguły r.
- Jeśli qb jest takim węzłem oraz q jest predykatem wbudowanym lub z EDB, odpowiedni podcel w r_a pozostaje taki sam jak w r.
- Jeśli q jest predykatem z IDB to zamieniamy predykat w odpowiednim podcelu r_a na q_b.
Jedynym problemem mogą być "aliasy", tzn. dwie zmienne w rzeczywistości oznaczające jedną. Oczyszczanie podcelów to metoda eliminująca aliasy. Powiemy, że podcel jest oczyszczony jeśli każdy z jego argumentów jest inną zmienną. Algorytm oczyszczania podcelów wygląda następująco:
- rozważamy podcel, w którym jakaś zmienna występuje więcej niż raz lub w którym występują stałe.
- Tworzymy nowy predykat p'(Y1,...,Ym) i podstawiamy go w miejsce p(X1,...,Xk) - Y1,...,Ym to wszystkie zmienne występujące pośród X1,...,Xk (m<k).
- Dla reguł dla p tworzymy odpowiadające reguły dla p' wykonując odpowiednie podstawienia analogicznie jak w poniższym przykładzie.
Przykład.
Zestaw reguł do oczyszczenia:
r1: p(X,Y) <- s(X,Y).
r2: p(X,Y) <- q(X,W), p(Y,Y).
Zgodnie z krokiem (1) tworzymy nowy predykat p' i wstawiamy go w miejsce p w drugim podcelu reguły r2.
r2: p(X,Y) <- q(X,W), p'(Y).
Reguły dla p' wyglądają następująco:
r3: p'(Y) <- s(Y,Y).
r4: p'(Y) <- q(Y,W), p'(Y).
Reguła r1 pozostaje bez zmian.
4. Algorytm magic-set.
Algorytm magic-set służy do przekształcenia reguł programu w nowy zbiór reguł, lecz dający tę samą odpowiedź na zapytanie. Algorytm oczekuje na wejściu zbioru reguł o unikalnych szablonach związania.
4.1 Algorytm przekształcania reguł.
- Dla każdego predykatu p z IDB tworzymy "magiczny" predykat m_p, którego argumentami są związane argumenty z p (szablony powiązań dla predykatów z IDB są różne).
- Dla każdej reguły rj, z k podcelami tworzymy predykaty uzupełniające supj.i, dla i=0,1,...,k-1.
- Argumentami predykatów uzupełniających tworzonych w punktach 2 i 3 poniższego algorytmu są zmienne, które
- Są związane w nagłówku (według szablonu związania).
- Występują w pierwszych i podcelach r oraz w dalszej części reguły lub w nagłówku (dla supr.i).
Następnie tworzymy reguły dla nowych predykatów, oraz nowe reguły dla predykatów z IDB:
- Reguły dla predykatów "magicznych". Rozważamy reguły zawierające w ciele predykat p z IDB. Niech p występuje w rj w i-tym podcelu postaci p(t1,...,tn), argumentami (i-1)-szej relacji uzupełniającej są zmienne U1,...,Ul, oraz związanymi argumentami z p według szablonu powiązań są argumenty i1,...,im. Tworzymy regułę:
m_p(ti1,...,tim) <- supj.i-1(U1,...,Ul).
- Reguły dla zerowych predykatów uzupełniających. Niech nagłówek reguły rj postaci p(t1,...,tn) ma związane argumenty i1,...,im. Niech X1,...,Xl będą zmiennymi występującymi wśród ti1,...,tim. Tworzymy regułę:
supj.0(X1,...,Xl) <- m_p(ti1,...,ti1,...,tim).
- Reguły dla pozostałych predykatów uzupełniających. Dla każdej reguły rj o k podcelach i dla każdego i=1,2,...,k-1 tworzymy regułę definiującą supj.i. Niech zmiennymi - atrybutami (i-1)-szej relacji uzupełniającej dla rj będą U1,...,Ul oraz niech zmiennymi - atrybutami i-tej relacji uzupełniającej będą V1,...,Vm. Niech p(t1,...,tn) będzie i-tym podcelem rj. Tworzymy regułę:
supj.i(V1,...,Vm) <- supj.i-1(U1,...,Ul),p(t1,...,tn).
- Reguły dla predykatów z IDB. Niech reguła rj ma k>=1 podcelów, k-tym podcelem będzie p(v1,...,vm), U1,...,Ul zmiennymi - atrybutami (k-1)-szej relacji uzupełniającej, a h(t1,...,tn) nagłówkiem rj. Tworzymy predykat:
h(t1,...,tn) <- supj.k-1(U1,...,Ul),p(v1,...,vm).
W przypadku, gdy k=0 predykat ma postać:
h(t1,...,tn) <- supj.0(U1,...,Ul).
- Reguła inicjująca. Niech q(s1,...,sn) będzie danym zapytaniem oraz i1,...,im będą związanymi argumentami zapytania. Tworzymy nowe zapytanie:
Przykład.
Program wejściowy:
r1: sg(X,Y) <- person(X).
r2: sg(X,Y) <- par(X,Xp), sg(Xp,Yp), par(Y,Yp).
Zapytanie: <- sg(a,W).
Reguły magic-set:
GRUPA 1:
(1) m_sg(Xp) <- sup2.1(X,Xp).
GRUPA 2:
(2) sup1.0(X) <- m_sg(X).
(3) sup2.0(X) <- m_sg(X).
GRUPA 3:
(4) sup2.1(X,Xp) <- sup2.0(X), par(X,Xp).
(5) sup2.2(X,Yp) <- sup2.1(X,Xp), sg(Xp, Yp).
GRUPA 4:
(6) sg(X,X) <- sup1.0(X), person(X).
(7) sg(X,Y) <- sup2.2(X,Yp), par(Y,Yp).
GRUPA 5:
(8) <- m_sg(a).
Po wygenerowaniu powyższego zbioru reguł możemy zastosować algorytm semi-naiwny do obliczeń. Obliczenia wykonują się w czasie proporcjonalnym do czasu wykonania algorytmu QRGT. Ma on jednak tę zaletę, że w przeciwieństwie do QRGT łatwo wykryć pełność odpowiedzi - gdy w kolejnym kroku nie wyliczą się żadne nowe fakty.
4.2 Uproszczenie reguł algorytmu magic-set.
Jeśli reguły dla pewnego predykatu p z IDB nie zawierają w ciele p, to można każdy podcel zawierający p zastąpić przez podstawienie w jego miejsce ciała reguł dla p. Nowe reguły mogą być mniej ogólne od starych, lecz relacje pozostają takie same.
Przykład.
r1: p(f(X),Y) <- t(X,Y).
r2: p(X,a) <- u(X), v(X).
r3: q(X,Y) <- p(X,Z), s(Z,Y).
t(r3): q(f(U),Y) <- p(f(U),V), s(V,Y).
t(r1): p(f(U),V) <- t(U,V).
r4: q(f(U),Y) <- t(U,V), s(V,Y).
r5: q(U,Y) <- u(U), v(U), s(a,Y).
Eliminacja reguł dla p ma jednak sens tylko wtedy gdy:
- jest tylko jedna reguła dla p i p występuje tylko w jednym podcelu lub.
- jest tylko jedna reguła dla p i w ciele tej reguły jest tylko jeden podcel.
W przypadku zbioru reguł magic-set uproszczeniu podlegają:
- zerowe predykaty uzupełniające (po jednej regule długości 1).
- predykaty uzupełniające supi.j dla i-tej reguły, jeśli j-tym podcelem jest predykat z EDB.
Często można też uprościć inne predykaty.
4.3 Uogólniony algorytm magic-set.
Dużą zaletą metod zstępujących jest to, że potrafią liczyć dokładnie to, co trzeba aby osiągnąć cel (dowód). Na przykład cel p(X,X,3) mówi, że dwa pierwsze argumenty są równe. SLD-rezolucja potrafi to wykorzystać, natomiast algorytm magic-set nie potrafi. Uogólniony algorytm magic-set przechowuje informacje nie tylko o argumentach związanych, ale o wszystkich. To wystarczy aby wykorzystać dodatkowe właściwości argumentów. Pojawia się jednak problem: niektóre "magiczne" reguły przestają być bezpieczne przy zapytaniach zawierających zmienne. Można rozwiązać ten problem dość łatwo wstawiając zmienne do krotek. Trzeba przy tym uważać na sytuacje, w których dwie krotki z różnymi zmiennymi tak naprawdę oznaczają tę samą krotkę (np. (A, 3) i (B, 3)), lub jedna jest bardziej ogólna (np. (A, A, 3) i (B, C, 3)).
5. Bibliografia.
Jeffrey D. Ullman, "Principles of Database and Knowledge-Base Systems, Volume II", Computer Science Press 1989, rozdziały 12 i 13 (str. 734-876)