sql >> Databáze >  >> RDS >> PostgreSQL

Jak vybrat pomocí klauzule WITH RECURSIVE

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



  1. Příklad Oracle WHILE LOOP

  2. Co znamená klíčové slovo KEY?

  3. Nelze VYBRAT z klauzule UPDATE RETURNING v postgresu

  4. Příklady DAYOFMONTH() – MySQL