Как выбрать, используя условие WITH RECURSIVE

У меня есть googled и прочтение эта страница руководства postgreSQL или эта страница блога и сам делал запросы с умеренным успехом (часть их зависает, а другие работают хорошо и быстро), но до сих пор я не могу полностью понять, как работает эта магия.

Может ли кто-нибудь дать очень четкое объяснение, демонстрирующее такую ​​семантику запроса и процесс выполнения, лучше на основе типичных образцов, таких как факториальный расчет или полное расширение дерева из таблицы (id,parent_id,name)?

И каковы основные ориентиры и типичные ошибки, которые нужно знать, чтобы сделать хорошие with recursive запросы?

Ответ 1

Прежде всего, попробуем упростить и уточнить описание алгоритма на странице . Чтобы упростить это, рассмотрим только 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

Или даже короче - механизм базы данных выполняет начальный выбор, беря его результирующие строки в качестве рабочего набора. Затем он повторно выполняет рекурсивный выбор в рабочем наборе, каждый раз заменяя содержимое рабочего набора на полученный результат запроса. Этот процесс заканчивается, когда пустой набор возвращается рекурсивным выбором. И все строки результатов, заданные сначала путем первоначального выбора, а затем путем рекурсивного выбора, собираются и подаются на внешний выбор, результатом которого становится общий результат запроса.

Этот запрос вычисляет factorial из 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 SELECT F*n F, n-1 n from factorial where n>1 указывает, что каждый раз, когда нам нужно умножить последнее значение funcion на значение последнего аргумента и значение аргумента декремента.
Database engine выполняет это следующим образом:

Прежде всего, он выполняет 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, который дает все элементы (корни) самого высокого уровня из таблицы 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

Затем он выполняет рекурсивный выбор, который дает все элементы второго уровня из таблицы 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 в факториальной выборке выше, выполнение рекурсивного выбора никогда не даст пустого набора (потому что у нас нет условия для фильтрации одной строки), и цикл будет продолжаться бесконечно.

Это наиболее вероятная причина, по которой некоторые из ваших запросов зависают (другая неспецифическая, но все же возможная причина - очень неэффективный выбор, который выполняется в конечном, но очень долгом времени).

Существует не так много запросов RECURSIVE guidlinesупомянуть, насколько я знаю. Но я хотел бы предложить (довольно очевидную) пошаговую процедуру рекурсивного построения запросов.

  • Отдельно создавайте и отлаживайте свой первоначальный выбор.

  • Оберните его лесками с конструкцией 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