Първо, нека се опитаме да опростим и изясним описанието на алгоритъма, дадено на страницата с ръководството. За да го опростите, разгледайте само union all
в with recursive
клауза засега (и union
по-късно):
WITH RECURSIVE pseudo-entity-name(column-names) AS (
Initial-SELECT
UNION ALL
Recursive-SELECT using pseudo-entity-name
)
Outer-SELECT using pseudo-entity-name
За да го изясним, нека опишем процеса на изпълнение на заявка в псевдокод:
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
Или дори по-кратко - Двигателят на базата данни изпълнява първоначален избор, приемайки редовете с резултати като работен набор. След това многократно изпълнява рекурсивно избиране на работния набор, като всеки път заменя съдържанието на работния набор с получения резултат от заявката. Този процес завършва, когато празен набор се връща чрез рекурсивно избиране. И всички резултативни редове, дадени първо чрез първоначален избор, а след това чрез рекурсивен избор, се събират и подават към външния избор, който резултат става общ резултат от заявката.
Тази заявка се изчислява факторно от 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
Първоначално изберете SELECT 1 F, 3 n
ни дава начални стойности:3 за аргумент и 1 за стойност на функцията.
Рекурсивно избиране SELECT F*n F, n-1 n from factorial where n>1
заявява, че всеки път, когато трябва да умножим стойността на последната функция по стойността на последния аргумент и да намалим стойността на аргумента.
Механизмът на база данни го изпълнява по следния начин:
Преди всичко изпълнява initail select, което дава първоначалното състояние на работния набор от записи:
F | n
--+--
1 | 3
След това трансформира работния набор от записи с рекурсивна заявка и получава второто си състояние:
F | n
--+--
3 | 2
След това трето състояние:
F | n
--+--
6 | 1
В третото състояние няма ред, който следва n>1
условие при рекурсивен избор, така нататък работният набор е изход от цикъл.
Външният набор от записи вече съдържа всички редове, върнати от начален и рекурсивен избор:
F | n
--+--
1 | 3
3 | 2
6 | 1
Външният избор филтрира всички междинни резултати от външния набор от записи, като показва само крайната факторна стойност, която става общ резултат от заявката:
F
--
6
И сега нека разгледаме таблицата 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
'Разгъване на цялото дърво ' тук означава сортиране на елементи от дърво в четим от човека ред в дълбочина, докато се изчисляват техните нива и (може би) пътища. И двете задачи (на правилно сортиране и изчисляване на ниво или път) не са разрешими в един (или дори постоянен брой) SELECT без използване на клауза WITH RECURSIVE (или клауза на Oracle CONNECT BY, която не се поддържа от PostgreSQL). Но тази рекурсивна заявка върши работата (е, почти върши, вижте бележката по-долу):
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
Двигателят за база данни го изпълнява по следния начин:
Първо, той изпълнява initail select, което дава всички елементи от най-високо ниво (корени) от forest
таблица:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
8 | | 1 | item 3 | item 3
6 | | 1 | item 2 | item 2
След това изпълнява рекурсивно избиране, което дава всички елементи от 2-ро ниво от forest
таблица:
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
След това отново изпълнява рекурсивно избиране, извличайки елементи от 3d ниво:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
И сега той отново изпълнява рекурсивно избиране, опитвайки се да извлече елементи от 4-то ниво, но няма нито един от тях, така че цикълът излиза.
Външният SELECT задава правилния четим от човека ред на редове, сортирайки по колоната на пътя:
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
ЗАБЕЛЕЖКА :Полученият ред на редовете ще остане правилен само докато няма препинателни знаци, предхождащи /
в имената на артикули. Ако преименуваме Item 2
в Item 1 *
, ще наруши реда на редовете, стоящ между Item 1
и неговите потомци.
По-стабилното решение е използването на табулаторен знак (E'\t'
) като разделител на пътя в заявка (който може да бъде заменен с по-четлив разделител на пътя по-късно:във външен избор, преди извеждане на човек или т.н.). Пътищата, разделени с табулатори, ще запазят правилния ред, докато в имената на елементите има табулатори или контролни знаци – които лесно могат да бъдат проверени и изключени без загуба на използваемост.
Много е лесно да промените последната заявка, за да разширите произволно поддърво - трябва само да замените условието parent_id is null
с perent_id=1
(например). Имайте предвид, че този вариант на заявка ще върне всички нива и пътища относно Item 1
.
А сега затипичните грешки . Най-забележимата типична грешка, специфична за рекурсивните заявки, е дефинирането на условия за лошо спиране в рекурсивния избор, което води до безкраен цикъл.
Например, ако пропуснем where n>1
условие във факторната извадка по-горе, изпълнението на рекурсивния избор никога няма да даде празен набор (тъй като нямаме условие за филтриране на един ред) и цикълът ще продължи безкрайно.
Това е най-вероятната причина, поради която някои от вашите заявки увисват (другата неспецифична, но все пак възможна причина е много неефективен избор, който се изпълнява за ограничено, но много дълго време).
Няма много насоки за заявки, специфични за РЕКУРСИВНО да спомена, доколкото знам. Но бих искал да предложа (доста очевидна) стъпка по стъпка процедура за изграждане на рекурсивна заявка.
-
Отделно изградете и отстранете грешки във вашия първоначален избор.
-
Увийте го със скеле WITH RECURSIVE конструкция
и започнете да изграждате и отстранявате грешки в рекурсивния си избор.
Препоръчителната конструкция за изтриване е следната:
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
Този най-прост външен избор ще изведе целия външен набор от записи, който, както знаем, съдържа всички изходни редове от първоначалния избор и всяко изпълнение на recusrive select в цикъл в техния оригинален изходен ред - точно както в примерите по-горе! limit 1000
част ще предотврати увисване, като я заменя с изход с голям размер, в който ще можете да видите пропуснатата точка на спиране.
- След отстраняване на грешки в началния и рекурсивния избор изградете и отстранете грешките във външния си избор.
И сега последното нещо, което трябва да споменем – разликата в използването на union
вместо union all
в with recursive
клауза. Той въвежда ограничение за уникалност на редове, което води до два допълнителни реда в нашия псевдокод за изпълнение:
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