Nejprve se pokusíme zjednodušit a zpřehlednit popis algoritmu uvedený na manuálové stránce. Pro zjednodušení zvažte pouze union all
v with recursive
prozatímní klauzule (a union
později):
WITH RECURSIVE pseudo-entity-name(column-names) AS (
Initial-SELECT
UNION ALL
Recursive-SELECT using pseudo-entity-name
)
Outer-SELECT using pseudo-entity-name
Abychom to objasnili, popišme proces provádění dotazu v pseudo kódu:
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
Nebo ještě kratší - Databázový stroj provede počáteční výběr a vezme jeho výsledné řádky jako pracovní sadu. Poté opakovaně provádí rekurzivní výběr na pracovní sadě, přičemž pokaždé nahradí obsah pracovní sady získaným výsledkem dotazu. Tento proces končí, když je rekurzivním výběrem vrácena prázdná množina. A všechny řádky výsledků dané nejprve počátečním výběrem a poté rekurzivním výběrem se shromáždí a přivedou do vnějšího výběru, jehož výsledek se stane celkovým výsledkem dotazu.
Tento dotaz vypočítává faktoriální ze 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
Počáteční výběr SELECT 1 F, 3 n
nám dává počáteční hodnoty:3 pro argument a 1 pro hodnotu funkce.
Rekurzivní výběr SELECT F*n F, n-1 n from factorial where n>1
uvádí, že pokaždé, když potřebujeme vynásobit hodnotu poslední funkce hodnotou posledního argumentu a snížit hodnotu argumentu.
Databázový stroj to provede takto:
Nejprve provede initail select, který dá počáteční stav pracovní sady záznamů:
F | n
--+--
1 | 3
Poté transformuje pracovní sadu záznamů pomocí rekurzivního dotazu a získá její druhý stav:
F | n
--+--
3 | 2
Poté třetí stav:
F | n
--+--
6 | 1
Ve třetím stavu není za n>1
žádný řádek podmínku v rekurzivním výběru, takže další pracovní sadou je ukončení smyčky.
Vnější sada záznamů nyní obsahuje všechny řádky vrácené počátečním a rekurzivním výběrem:
F | n
--+--
1 | 3
3 | 2
6 | 1
Vnější výběr odfiltruje všechny mezivýsledky z vnější sady záznamů a zobrazí pouze konečnou faktoriálovou hodnotu, která se stane celkovým výsledkem dotazu:
F
--
6
A nyní se podívejme na tabulku 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
'Rozbalování celého stromu ' zde znamená třídění položek stromu v lidsky čitelném pořadí hloubky-první při výpočtu jejich úrovní a (možná) cest. Obě úlohy (správného řazení a výpočtu úrovně nebo cesty) nejsou řešitelné v jednom (nebo dokonce libovolném konstantním počtu) SELECT bez použití klauzule WITH RECURSIVE (nebo klauzule Oracle CONNECT BY, kterou PostgreSQL nepodporuje). Ale tento rekurzivní dotaz dělá svou práci (no, skoro ano, viz poznámka níže):
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
Databázový stroj to provede takto:
Nejprve provede initail select, který poskytne všechny položky nejvyšší úrovně (kořeny) z forest
tabulka:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
8 | | 1 | item 3 | item 3
6 | | 1 | item 2 | item 2
Poté provede rekurzivní výběr, který poskytne všechny položky 2. úrovně z forest
tabulka:
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
Poté znovu provede rekurzivní výběr a načte položky 3D úrovně:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
A nyní znovu provede rekurzivní výběr, pokusí se získat položky 4. úrovně, ale žádné z nich nejsou, takže se smyčka ukončí.
Vnější SELECT nastavuje správné pořadí řádků čitelné pro člověka, řazení podle sloupce cesty:
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
POZNÁMKA :Výsledné pořadí řádků zůstane správné pouze v případě, že před /
nebudou uvedeny žádné interpunkční znaky v názvech položek. Pokud přejmenujeme Item 2
v Item 1 *
, přeruší pořadí řádků, stojící mezi Item 1
a jeho potomci.
Stabilnějším řešením je použití znaku tabulátor (E'\t'
) jako oddělovač cest v dotazu (který lze později nahradit čitelnějším oddělovačem cest:ve vnějším výběru, před zobrazením člověku atd.). Cesty oddělené tabulátory si zachovají správné pořadí, dokud v názvech položek nebudou tabulátory nebo řídicí znaky – což lze snadno zkontrolovat a vyloučit bez ztráty použitelnosti.
Je velmi jednoduché upravit poslední dotaz tak, aby se rozšířil libovolný podstrom – stačí pouze nahradit podmínku parent_id is null
s perent_id=1
(například). Tato varianta dotazu vrátí všechny úrovně a cesty vzhledem k Item 1
.
A nyní o typických chybách . Nejpozoruhodnější typickou chybou specifickou pro rekurzivní dotazy je definování podmínek špatného zastavení v rekurzivním výběru, což má za následek nekonečnou smyčku.
Pokud například vynecháme where n>1
podmínky ve faktoriálním vzorku výše, provedení rekurzivního výběru nikdy neposkytne prázdnou množinu (protože nemáme žádnou podmínku pro odfiltrování jednoho řádku) a opakování bude pokračovat donekonečna.
To je nejpravděpodobnější důvod, proč některé z vašich dotazů visí (dalším nespecifickým, ale stále možným důvodem je velmi neefektivní výběr, který se provádí v konečném, ale velmi dlouhém čase).
Není mnoho pokynů pro dotazování specifické pro REKURSIVNÍ zmínit, pokud vím. Ale rád bych navrhl (spíše zřejmý) postup vytváření rekurzivního dotazu krok za krokem.
-
Samostatně sestavte a odlaďte svůj počáteční výběr.
-
Obalte to lešením S REKURSIVNÍ konstrukcí
a začněte budovat a ladit svůj rekurzivní výběr.
Doporučená konstrukce scufffoldingu je tato:
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
Tento nejjednodušší vnější výběr vygeneruje celou vnější sadu záznamů, která, jak víme, obsahuje všechny výstupní řádky od počátečního výběru a každé provedení rekurzivního výběru ve smyčce v jejich původním výstupním pořadí – stejně jako ve výše uvedených ukázkách! limit 1000
část zabrání zavěšení a nahradí ji předimenzovaným výstupem, ve kterém budete moci vidět zmeškaný bod zastavení.
- Po odladění počátečního a rekurzivního výběru vytvořte a odlaďte svůj vnější výběr.
A teď poslední věc, kterou je třeba zmínit – rozdíl v použití union
místo union all
v with recursive
doložka. Zavádí omezení jedinečnosti řádku, což má za následek dva řádky navíc v našem prováděcím pseudokódu:
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