воскресенье, 13 ноября 2016 г.

Работа с иерархически организованными данными в Oracle 11gR2

Данные, организованные иерархически, - дерево или деревья - чаще всего представлены в реляционной БД как список узлов, где дочерний узел ссылается на родительский.

Создадим и заполним таблицу locations, иллюстрирующую именно этот подход:

create table locations (
    id number primary key,
    parent number references locations (id) on delete cascade,
    name varchar2(50) not null
);

insert into locations values (1, null, 'Земля');
insert into locations values (2, 1, 'Азия');
insert into locations values (3, 2, 'Владивосток');
insert into locations values (4, 2, 'Саппоро');
insert into locations values (5, 2, 'Стамбул');
insert into locations values (6, 1, 'Европа');
insert into locations values (7, 6, 'Реймс');
insert into locations values (8, 6, 'Санкт-Петербург');

Здесь узел 'Земля' есть корень дерева, на него ссылаются узлы 'Азия' и 'Европа', на них ссылаются остальные узлы - листья. Здесь каждый узел

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

Какими средствами языка SQL можно извлечь все узлы некоторого поддерева? А все узлы, лежащие на пути от данного узла до корня дерева?

Традиционно в СУБД Oracle это делается с помощью иерархического запроса. В таком запросе используется предложение connect by для описания связей между родительскими и дочерними узлами, и предложение start with - для задания узла (узлов), с которого нужно начать построение иерархии. Псевдостолбец level и функции sys_connect_by_path и connect_by_root позволяют получить для узла иерархии его уровень, путь к нему от корня и корень построенной иерархии, соответственно.

Вот иерархический запрос, возвращающий все узлы дерева, начиная с корня - узла 'Земля'. Значение level используется, чтобы сдвинуть название узла вправо на (level-1)*2 пробелов и наглядно представить уровни дерева:

-- от корня к листьям, или получить все узлы-потомки
select id, parent, rpad(' ', (level-1)*2) || name, level, sys_connect_by_path(id, '/') path, connect_by_root(id) root
from locations
connect by prior id = parent
start with parent is null
;

    ID PARENT RPAD('',(LEVEL-1)*2)||NAME   LEVEL  PATH                 ROOT
------ ------ ---------------------------- ------ -------------------- ------
     1        Земля                             1 /1                        1
     2      1   Азия                            2 /1/2                      1
     3      2     Владивосток                   3 /1/2/3                    1
     4      2     Саппоро                       3 /1/2/4                    1
     5      2     Стамбул                       3 /1/2/5                    1
     6      1   Европа                          2 /1/6                      1
     7      6     Реймс                         3 /1/6/7                    1
     8      6     Санкт-Петербург               3 /1/6/8                    1

8 rows selected

Следующий запрос выполняет обратный проход от узлов-городов, заданных в предложении start with, к узлу 'Земля' - терминальному для построенных иерархий:

-- от листьев к корню, или получить все узлы-предки
select id, parent, rpad(' ', (level-1)*2) || name, level, sys_connect_by_path(id, '/') path, connect_by_root(id) root
from locations
connect by prior parent = id
start with id not in (select nvl(parent,-1) from locations)
;

    ID PARENT RPAD('',(LEVEL-1)*2)||NAME   LEVEL  PATH                 ROOT
------ ------ ---------------------------- ------ -------------------- ------
     3      2 Владивосток                       1 /3                        3
     2      1   Азия                            2 /3/2                      3
     1            Земля                         3 /3/2/1                    3
     4      2 Саппоро                           1 /4                        4
     2      1   Азия                            2 /4/2                      4
     1            Земля                         3 /4/2/1                    4
     5      2 Стамбул                           1 /5                        5
     2      1   Азия                            2 /5/2                      5
     1            Земля                         3 /5/2/1                    5
     7      6 Реймс                             1 /7                        7
     6      1   Европа                          2 /7/6                      7
     1            Земля                         3 /7/6/1                    7
     8      6 Санкт-Петербург                   1 /8                        8
     6      1   Европа                          2 /8/6                      8
     1            Земля                         3 /8/6/1                    8

15 rows selected

Начиная с версии Oracle 11gR2 для работы с иерархически организованными данными поддерживаются так называемые recursive common table expressions (CTEs), описанные в стандарте SQL-99. Обратите внимание, как (очевидным образом) строятся значения столбцов lvl (уровень), path и root при использовании CTEs в следующих запросах.

-- от корня к листьям, или получить все узлы-потомки
with tree (id, parent, name, lvl, path, root) as (
    select id, parent, name, 1, '/'||id, id
    from locations
    where parent is null
    union all
    select l.id, l.parent, l.name, t.lvl+1, t.path||'/'||l.id, t.root
    from locations l join tree t on l.parent = t.id
)
select id, parent, rpad(' ', (lvl-1)*2) || name, lvl, path, root from tree
order by path
;

    ID PARENT RPAD('',(LEVEL-1)*2)||NAME   LEVEL  PATH                 ROOT
------ ------ ---------------------------- ------ -------------------- ------
     1        Земля                             1 /1                        1
     2      1   Азия                            2 /1/2                      1
     3      2     Владивосток                   3 /1/2/3                    1
     4      2     Саппоро                       3 /1/2/4                    1
     5      2     Стамбул                       3 /1/2/5                    1
     6      1   Европа                          2 /1/6                      1
     7      6     Реймс                         3 /1/6/7                    1
     8      6     Санкт-Петербург               3 /1/6/8                    1

8 rows selected

-- от листьев к корню, или получить все узлы-предки
with tree (id, parent, name, lvl, path, root) as (
    select id, parent, name, 1, '/'||id, id
    from locations
    where id not in (select nvl(parent, -1) from locations)
    union all
    select l.id, l.parent, l.name, t.lvl+1, t.path||'/'||l.id, t.root
    from locations l join tree t on l.id = t.parent
)
select id, parent, rpad(' ', (lvl-1)*2) || name, lvl, path, root from tree
order by path
;

    ID PARENT RPAD('',(LEVEL-1)*2)||NAME   LEVEL  PATH                 ROOT
------ ------ ---------------------------- ------ -------------------- ------
     3      2 Владивосток                       1 /3                        3
     2      1   Азия                            2 /3/2                      3
     1            Земля                         3 /3/2/1                    3
     4      2 Саппоро                           1 /4                        4
     2      1   Азия                            2 /4/2                      4
     1            Земля                         3 /4/2/1                    4
     5      2 Стамбул                           1 /5                        5
     2      1   Азия                            2 /5/2                      5
     1            Земля                         3 /5/2/1                    5
     7      6 Реймс                             1 /7                        7
     6      1   Европа                          2 /7/6                      7
     1            Земля                         3 /7/6/1                    7
     8      6 Санкт-Петербург                   1 /8                        8
     6      1   Европа                          2 /8/6                      8
     1            Земля                         3 /8/6/1                    8

15 rows selected

Результаты упорядочены по столбцу path, чтобы выглядеть так же, как результаты традиционных connect by-запросов, приведенных выше.

Теперь, на примере иерархии locations рассмотрим основные операции на дереве:

  • добавить узел,
  • переместить узел,
  • удалить узел.

Добавляется всегда терминальный узел (лист дерева). С помощью этой операции выше была заполнена таблица locations.

Переместить можно произвольный узел, и, если этот узел не терминальный, то перемещается поддерево, корнем которого он является. Технически ничто не мешает переместить узел с уровня на уровень; то есть, узел в дереве можно переместить горизонтально, вертикально, или по диагонали.

SQL> -- перенос Стамбула в Европу
SQL> update locations set parent = 6 where name = 'Стамбул';
1 row updated

SQL> -- перенос Азии в Европу
SQL> update locations set parent = (select id from locations where name = 'Европа') 
  2  where name = 'Азия';
1 row updated

SQL> with tree (id, parent, name, lvl, path, root) as (
  2      select id, parent, name, 1, '/'||id, id
  3      from locations
  4      where parent is null
  5      union all
  6      select l.id, l.parent, l.name, t.lvl+1, t.path||'/'||l.id, t.root
  7      from locations l join tree t on l.parent = t.id
  8  )
  9  select id, parent, rpad(' ', (lvl-1)*2) || name, lvl, path, root from tree
 10  order by path
 11  ;

    ID PARENT RPAD('',(LEVEL-1)*2)||NAME   LEVEL  PATH                 ROOT
------ ------ ---------------------------- ------ -------------------- ------
     1        Земля                             1 /1                        1
     6      1   Европа                          2 /1/6                      1
     2      6     Азия                          3 /1/6/2                    1
     3      2       Владивосток                 4 /1/6/2/3                  1
     4      2       Саппоро                     4 /1/6/2/4                  1
     5      6     Стамбул                       3 /1/6/5                    1
     7      6     Реймс                         3 /1/6/7                    1
     8      6     Санкт-Петербург               3 /1/6/8                    1

8 rows selected

Перемещение или добавление узла может привести к появлению цикла - и разрушению дерева:

SQL> update locations set parent = (select id from locations where name = 'Азия') where name = 'Европа';
1 row updated

SQL> with tree (id, parent, name, lvl, path, root) as (
  2      select id, parent, name, 1, '/'||id, id
  3      from locations
  4      where id = 6
  5      union all
  6      select l.id, l.parent, l.name, t.lvl+1, t.path||'/'||l.id, t.root
  7      from locations l join tree t on l.parent = t.id
  8  )
  9  select id, parent, rpad(' ', (lvl-1)*2) || name, lvl, path, root from tree
 10  order by path
 11  ;

ORA-32044: cycle detected while executing recursive WITH query

SQL> rollback;
Rollback complete

Удалить можно произвольный узел. Если удаляется нетерминальный узел, то удаляется поддерево, корнем которого он является:

SQL> delete from locations where name = 'Азия';
1 row deleted

SQL> with tree (id, parent, name, lvl, path, root) as (
  2      select id, parent, name, 1, '/'||id, id
  3      from locations
  4      where parent is null
  5      union all
  6      select l.id, l.parent, l.name, t.lvl+1, t.path||'/'||l.id, t.root
  7      from locations l join tree t on l.parent = t.id
  8  )
  9  select id, parent, rpad(' ', (lvl-1)*2) || name, lvl, path, root from tree
 10  order by path
 11  ;

    ID PARENT RPAD('',(LEVEL-1)*2)||NAME   LEVEL  PATH                 ROOT
------ ------ ---------------------------- ------ -------------------- ------
     1        Земля                             1 /1                        1
     6      1   Европа                          2 /1/6                      1
     7      6     Реймс                         3 /1/6/7                    1
     8      6     Санкт-Петербург               3 /1/6/8                    1

Удаление поддерева возможно потому, что ссылочное ограничение целостности на таблице locations определено с опцией on delete cascade. Если бы не это, удаление нетерминальных узлов было бы невозможно.

Представление иерархически организованных данных, проиллюстрированное с помощью таблицы locations, - когда строка таблицы представляет узел, ссылающийся на родительский - не единственно возможное. Поскольку каждый узел дерева имеет уникальный путь, веудущий к нему от корня через промежуточные узлы, то дерево можно представить как список таких путей. Или как список путей от корня к листьям. Выбор того или иного представления зависит от решаемой задачи. Несколько способов хранения деревьев в реляционной БД, их преимущества и недостатки описаны, например, в книге SQL Antipatterns: Avoiding the Pitfalls of Database Programming by Bill Karwin.

В заключение, удаляю таблицу, которая больше не нужна:

SQL> drop table locations;
Table dropped

Комментариев нет:

Отправить комментарий