Przede wszystkim spróbujmy uprościć i doprecyzować opis algorytmu podany na stronie podręcznika. Aby to uprościć, rozważ tylko union all
w with recursive
na razie klauzula (i union
później):
WITH RECURSIVE pseudo-entity-name(column-names) AS (
Initial-SELECT
UNION ALL
Recursive-SELECT using pseudo-entity-name
)
Outer-SELECT using pseudo-entity-name
Aby to wyjaśnić, opiszmy proces wykonywania zapytania w pseudokodzie:
working-recordset = result of Initial-SELECT
append working-recordset to empty outer-recordset
while( working-recordset is not empty ) begin
new working-recordset = result of Recursive-SELECT
taking previous working-recordset as pseudo-entity-name
append working-recordset to outer-recordset
end
overall-result = result of Outer-SELECT
taking outer-recordset as pseudo-entity-name
Lub nawet krócej — aparat bazy danych wykonuje wstępne zaznaczenie, przyjmując wiersze wyników jako zestaw roboczy. Następnie wielokrotnie wykonuje rekurencyjne wybieranie na zbiorze roboczym, za każdym razem zastępując zawartość zbioru roboczego otrzymanym wynikiem zapytania. Ten proces kończy się, gdy rekurencyjny wybór zwraca pusty zestaw. Wszystkie wiersze wyników podane najpierw przez wybór początkowy, a następnie przez wybór rekurencyjny są gromadzone i przesyłane do zewnętrznego wyboru, który to wynik staje się ogólnym wynikiem zapytania.
To zapytanie oblicza silnik z 3:
WITH RECURSIVE factorial(F,n) AS (
SELECT 1 F, 3 n
UNION ALL
SELECT F*n F, n-1 n from factorial where n>1
)
SELECT F from factorial where n=1
Wybór początkowy SELECT 1 F, 3 n
daje nam wartości początkowe:3 dla argumentu i 1 dla wartości funkcji.
Wybierz rekurencyjnie SELECT F*n F, n-1 n from factorial where n>1
stwierdza, że za każdym razem musimy pomnożyć wartość ostatniej funkcji przez wartość ostatniego argumentu i zmniejszyć wartość argumentu.
Silnik bazy danych wykonuje to w następujący sposób:
Przede wszystkim wykonuje initail select, który daje początkowy stan działającego zestawu rekordów:
F | n
--+--
1 | 3
Następnie przekształca działający zestaw rekordów za pomocą zapytania rekurencyjnego i uzyskuje jego drugi stan:
F | n
--+--
3 | 2
Następnie trzeci stan:
F | n
--+--
6 | 1
W trzecim stanie nie ma wiersza następującego po n>1
warunek w rekurencyjnym wyborze, więc kolejnym zbiorem roboczym są wyjścia z pętli.
Zewnętrzny zestaw rekordów zawiera teraz wszystkie wiersze zwrócone przez wybór początkowy i rekurencyjny:
F | n
--+--
1 | 3
3 | 2
6 | 1
Zewnętrzny wybór odfiltrowuje wszystkie pośrednie wyniki z zewnętrznego zestawu rekordów, pokazując tylko końcową wartość silni, która staje się ogólnym wynikiem zapytania:
F
--
6
A teraz rozważmy tabelę forest(id,parent_id,name)
:
id | parent_id | name
---+-----------+-----------------
1 | | item 1
2 | 1 | subitem 1.1
3 | 1 | subitem 1.2
4 | 1 | subitem 1.3
5 | 3 | subsubitem 1.2.1
6 | | item 2
7 | 6 | subitem 2.1
8 | | item 3
'Rozwijanie pełnego drzewa Oznacza tutaj sortowanie elementów drzewa w czytelnej dla człowieka kolejności w głąb przy obliczaniu ich poziomów i (być może) ścieżek. Oba zadania (prawidłowego sortowania i obliczania poziomu lub ścieżki) nie są możliwe do rozwiązania w jednym (lub nawet stałej liczbie) SELECT bez użycia klauzuli WITH RECURSIVE (lub klauzuli Oracle CONNECT BY, która nie jest obsługiwana przez PostgreSQL). Ale to rekurencyjne zapytanie spełnia swoje zadanie (no, prawie tak, patrz uwaga poniżej):
WITH RECURSIVE fulltree(id,parent_id,level,name,path) AS (
SELECT id, parent_id, 1 as level, name, name||'' as path from forest where parent_id is null
UNION ALL
SELECT t.id, t.parent_id, ft.level+1 as level, t.name, ft.path||' / '||t.name as path
from forest t, fulltree ft where t.parent_id = ft.id
)
SELECT * from fulltree order by path
Silnik bazy danych wykonuje to w następujący sposób:
Po pierwsze, wykonuje initail select, który daje wszystkie elementy najwyższego poziomu (root) z forest
tabela:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
8 | | 1 | item 3 | item 3
6 | | 1 | item 2 | item 2
Następnie wykonuje rekurencyjne zaznaczanie, które daje wszystkie elementy drugiego poziomu z forest
tabela:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
2 | 1 | 2 | subitem 1.1 | item 1 / subitem 1.1
3 | 1 | 2 | subitem 1.2 | item 1 / subitem 1.2
4 | 1 | 2 | subitem 1.3 | item 1 / subitem 1.3
7 | 6 | 2 | subitem 2.1 | item 2 / subitem 2.1
Następnie ponownie wykonuje rekurencyjne zaznaczanie, pobierając elementy poziomu 3D:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
A teraz ponownie wykonuje rekurencyjne zaznaczenie, próbując pobrać elementy czwartego poziomu, ale ich nie ma, więc pętla się kończy.
Zewnętrzny SELECT ustawia prawidłową, czytelną dla człowieka kolejność wierszy, sortując według kolumny ścieżki:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
2 | 1 | 2 | subitem 1.1 | item 1 / subitem 1.1
3 | 1 | 2 | subitem 1.2 | item 1 / subitem 1.2
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
4 | 1 | 2 | subitem 1.3 | item 1 / subitem 1.3
6 | | 1 | item 2 | item 2
7 | 6 | 2 | subitem 2.1 | item 2 / subitem 2.1
8 | | 1 | item 3 | item 3
UWAGA :Wynikowa kolejność wierszy pozostanie poprawna tylko wtedy, gdy nie będzie żadnych znaków interpunkcyjnych poprzedzających sortowanie /
w nazwach przedmiotów. Jeśli zmienimy nazwę Item 2
w Item 1 *
, zepsuje kolejność wierszy, stojąc między Item 1
i jego potomków.
Bardziej stabilnym rozwiązaniem jest użycie znaku tabulacji (E'\t'
) jako separator ścieżki w zapytaniu (który można później zastąpić bardziej czytelnym separatorem ścieżki:w zewnętrznym zaznaczeniu, przed wyświetleniem do człowieka itp.). Ścieżki oddzielone tabulatorami zachowają prawidłową kolejność, dopóki w nazwach elementów nie pojawią się tabulatory lub znaki kontrolne - co można łatwo sprawdzić i wykluczyć bez utraty użyteczności.
Modyfikowanie ostatniego zapytania w celu rozwinięcia dowolnego poddrzewa jest bardzo proste - wystarczy podstawić warunek parent_id is null
z perent_id=1
(na przykład). Pamiętaj, że ten wariant zapytania zwróci wszystkie poziomy i ścieżki względem Item 1
.
A teraz o typowych błędach . Najbardziej zauważalnym typowym błędem charakterystycznym dla zapytań rekurencyjnych jest zdefiniowanie warunków złego zatrzymania w rekurencyjnym wyborze, co skutkuje nieskończoną pętlą.
Na przykład, jeśli pominiemy where n>1
warunek w próbce silniowej powyżej, wykonanie rekurencyjnego wyboru nigdy nie da pustego zestawu (ponieważ nie mamy warunku do odfiltrowania pojedynczego wiersza), a pętla będzie kontynuowana w nieskończoność.
Jest to najbardziej prawdopodobny powód, dla którego niektóre z Twoich zapytań zawieszają się (innym niespecyficznym, ale wciąż możliwym powodem jest bardzo nieefektywny wybór, który wykonuje się w skończonym, ale bardzo długim czasie).
Nie ma wiele wskazówek dotyczących zapytań REKURSOWYCH wspomnieć, o ile wiem. Ale chciałbym zasugerować (raczej oczywistą) procedurę rekurencyjnego budowania zapytań krok po kroku.
-
Oddzielnie skompiluj i debuguj swój początkowy wybór.
-
Owiń to rusztowaniem Z konstrukcją REKURSOWNĄ
i zacznij budować i debugować wybór rekurencyjny.
Zalecana konstrukcja rusztowania jest następująca:
WITH RECURSIVE rec( <Your column names> ) AS (
<Your ready and working initial SELECT>
UNION ALL
<Recursive SELECT that you are debugging now>
)
SELECT * from rec limit 1000
Ten najprostszy wybór zewnętrzny wygeneruje cały zewnętrzny zestaw rekordów, który, jak wiemy, zawiera wszystkie wiersze wyjściowe z początkowego wyboru i każde wykonanie rekurencyjnego wyboru w pętli w oryginalnej kolejności wyjściowej — tak jak w powyższych przykładach! limit 1000
część zapobiegnie zawieszeniu, zastępując ją zbyt dużym wyjściem, w której będziesz mógł zobaczyć pominięty punkt zatrzymania.
- Po debugowaniu początkowym i rekurencyjnym wybierz kompilację i debuguj zewnętrzny wybór.
A teraz ostatnia rzecz, o której należy wspomnieć - różnica w używaniu union
zamiast union all
w with recursive
klauzula. Wprowadza ograniczenie unikalności wiersza, co skutkuje dwoma dodatkowymi wierszami w naszym pseudokodzie wykonania:
working-recordset = result of Initial-SELECT
discard duplicate rows from working-recordset /*union-specific*/
append working-recordset to empty outer-recordset
while( working-recordset is not empty ) begin
new working-recordset = result of Recursive-SELECT
taking previous working-recordset as pseudo-entity-name
discard duplicate rows and rows that have duplicates in outer-recordset
from working-recordset /*union-specific*/
append working-recordset to outer-recordset
end
overall-result = result of Outer-SELECT
taking outer-recordset as pseudo-entity-name