Чтобы понять рекурсию, сначала надо понять рекурсию.
Здесь пойдет речь об иерархичных запросах, которые часто называют рекурсивными, но будет правильнее назвать их иерархичными запросами с рекурсивным поведением.
Для дальнейших экспериментов создадим таблицу с иерархией времен года и месяцев.
create table table_tmp
(id number,
parent_id number,
name varchar2(50));
Заполним
insert into table_tmp values(1, null, 'Year');
insert into table_tmp values(2, 1, 'Winter');
insert into
table_tmp values(3, 2, 'December');
insert into
table_tmp values(4, 2, 'January');
insert into
table_tmp values(5, 1, 'Spring');
insert into
table_tmp values(6, 5, 'March');
insert into table_tmp values(7, 5, 'April');
insert into
table_tmp values(8, 5, 'May');
insert into
table_tmp values(9, 1, 'Summer');
insert into
table_tmp values(10, 9, 'June');
insert into
table_tmp values(11, 9, 'July');
insert into
table_tmp values(12, 9, 'August');
insert into
table_tmp values(13, 1, 'Autumn');
insert into
table_tmp values(14, 13, 'September');
insert into
table_tmp values(15, 13, 'October');
insert into
table_tmp values(16, 13, 'November');
Немного теории.
Описание синтаксиса в документации:
Единственно важное условие для построения иерархического запроса - оператор CONNECT BY, остальное по необходимости.
Самый простой пример использования - генерация последовательности чисел. Например, месяцы от 1 до 12:
select level r from dual connect by level <= 12;
Условие после CONNECT BY обязательно. Здесь указываем сколько продолжать цикл.
Хозяйке на заметку: если нужно размножить некую строку, к примеру на 10 строк, то это можно сделать вот так:
select 1 as value r from dual connect by rownum <= 10;
LEVEL - это псевдостолбец, в нем записывается уровень записи по отношению к корневой. 1-ая запись будет иметь уровень 1, ее потомки - уровень 2, потомки потомков - уровень 3, etc.
Но в случае генерации чисел этого не видно, потому что иерархия фактически не построена.
Определимся сначала откуда стартовать.
START WITH - необязательный оператор, здесь указываем какая строка/строки будет корневой. Условие может быть числом, литералом, null и даже функцией - substr(column,1,1) = 'A' - или внутренним запросом.
Для построения иерархии нужен специальный оператор PRIOR. Это обычный унарный оператор, такой же как + или -, только он указывает на предыдущую в иерархии запись, грубо говоря, минус 1 уровень.
parent_id = PRIOR id
Итак, иерархичный запрос будет следующим:
select lpad(' ',3 * level)||name as tree, -- выстраиваем визуальное дерево
sys_connect_by_path(name, '/') as Path -- собираем путь, выглядящий как путь к
файлу к папке
from table_tmp
start with parent_id is null
--- задаем старт иерархии
connect by prior id = parent_id -- увязываем
order siblings by id;
-- сортируем в пределах общего родителя, если order by без siblings иерархия
при сортировке визуально развалится
Результат:
TREE |
PATH |
Year |
/Year |
Winter |
/Year/Winter |
December |
/Year/Winter/December |
January |
/Year/Winter/January |
February |
/Year/Winter/February |
Spring |
/Year/Spring |
March |
/Year/Spring/March |
April |
/Year/Spring/April |
May |
/Year/Spring/May |
Summer |
/Year/Summer |
June |
/Year/Summer/June |
July |
/Year/Summer/Julay |
August |
/Year/Summer/August |
Autumn |
/Year/Autumn |
September |
/Year/Autumn/September |
October |
/Year/Autumn/October |
November |
/Year/Autumn/November |
Еще может быть полезен псевдостолбец CONNECT_BY_ISLEAF. Его можно использовать так же, как LEVEL. В этом псевдостолбце напротив каждой строки проставляется 0 или 1. Если есть потомки – проставится 0. Если потомков нет, такой узел в дереве называется “листом”, тогда и значение в поле CONNECT_BY_ISLEAF будет равно 1.
Помимо оператора PRIOR, который ссылался к родительской записи, есть другой унарный оператор CONNECT_BY_ROOT, который ссылается на корневую запись, т.е. на самую первую в выборке.
select id,
Результат:
ID |
PARENT_ID |
NAME |
LEVEL |
ISLEAF |
PARENT_NAME |
ROOT |
1 |
|
Year |
1 |
0 |
|
Year |
2 |
1 |
Winter |
2 |
0 |
Year |
Year |
3 |
2 |
December |
3 |
1 |
Winter |
Year |
4 |
2 |
January |
3 |
1 |
Winter |
Year |
4 |
2 |
February |
3 |
1 |
Winter |
Year |
5 |
1 |
Spring |
2 |
0 |
Year |
Year |
6 |
5 |
March |
3 |
1 |
Spring |
Year |
7 |
5 |
April |
3 |
1 |
Spring |
Year |
8 |
5 |
May |
3 |
1 |
Spring |
Year |
9 |
1 |
Summer |
2 |
0 |
Year |
Year |
10 |
9 |
June |
3 |
1 |
Summer |
Year |
11 |
9 |
Julay |
3 |
1 |
Summer |
Year |
12 |
9 |
August |
3 |
1 |
Summer |
Year |
13 |
1 |
Autumn |
2 |
0 |
Year |
Year |
14 |
13 |
September |
3 |
1 |
Autumn |
Year |
15 |
13 |
October |
3 |
1 |
Autumn |
Year |
16 |
13 |
November |
3 |
1 |
Autumn |
Year |
Все это результаты правильно построенной в таблице иерархии, когда есть все основные элементы иерархии: root, parent, child, и они между собой правильно взаимосвязаны и становятся branch или leaf.
Если иерархия построена неправильно, то произойдет либо выпадение уровней, либо ошибка ORA-01436.
Изменим иерархию - проставим второму уровню Autumn, который соотвествует временам года, parent_id равный ID сентября (September)
update table_tmp set parent_id=14 where id=13;
Если запустим селект с построением дерева, то получим то, что время года Autumn и его потомки, выпадут из иерархии. Это произошло потому, что построение началось с корня/предка, а Autumn своего предка "потерял". В принципе можно було проставить любое другое число, главное оборвать связь с корнем.
Результат наглядно - Autumn и его потомки отсутствуют.
TREE |
PATH |
Year |
/Year |
Winter |
/Year/Winter |
December |
/Year/Winter/December |
January |
/Year/Winter/January |
February |
/Year/Winter/February |
Spring |
/Year/Spring |
March |
/Year/Spring/March |
April |
/Year/Spring/April |
May |
/Year/Spring/May |
Summer |
/Year/Summer |
June |
/Year/Summer/June |
July |
/Year/Summer/Julay |
August |
/Year/Summer/August |
Но если попробовать сейчас простроить иерархию осени - Autumn, то есть start with id = 13, то выпадет ошибка:
ORA-01436: CONNECT BY loop in user data.
Это так называемая "петля".
Если в
данных содержатся "петли" – это явно ошибка, в деревьях не бывает
петель.
Для этого служит оператор NOCYCLE после CONNECT BY.
А чтобы определить “больной” участок, используется псевдостолбец CONNECT_BY_ISCYCLE
– в нем во всех "хороших" строках будет записано 0, а в тех, которые
приводят к петлям, окажется 1. Применяется только с NOCYCLE.
ISCYCLE |
ID |
PARENT_ID |
NAME |
LEVEL |
0 |
14 |
13 |
September |
1 |
1 |
13 |
14 |
Autumn |
2 |
0 |
15 |
13 |
October |
3 |
0 |
16 |
13 |
November |
3 |
0 |
15 |
13 |
October |
1 |
0 |
16 |
13 |
November |
1 |
Практические случаи:
1) Найти пропущенные дни. Возьмем массив из 7 дней (недели) с произвольными датами, и пропустим среду и пятницу, то есть 3 и 5 день. Найдем эти дни.
2) Преобразовать строку в столбец:
with t_data as (select '1;2;3;4;5' as value from dual)
или похожая задача - распарсить строки:
with t_data as (select '1;Sea;Water;4;5' as value, 1 id from dual union all
Зачем нужна строка prior dbms_random.value is not null?
Если ее не будет, то получим знакомую ошибку "петли" - ORA-01436: CONNECT BY loop in user data.
Это связано с тем, как Oracle определяет зацикливание: если у очередной строки значения всех полей, которые употребляются с prior, равно значению этих полей из предыдущего уровня рекурсии, значит запрос зациклился. На каждом уровне хотя бы одно поле, употребленное с prior, должно отличаться. Это и обеспечивает DBMS_RANDOM, дающий подряд неповторяющиеся значения.
А как видно из содержимого WITH такого не будет, ID для пяти строк после распарсивания будет одинаковый.
Также вместо dbms_random.value многие советут использовать sys_guid, чтобы обойтись без PL/SQL контекста.
and prior sys_guid() is not null
3) И просто построить иерархию, визуализируя ее, например для отчетности, как в примерах выше - через отступы, добавление символов или построение пути.
Продолжение следует...
Иерархические (рекурсивные) запросы
0 Комментарии