Моделирование иерархических объектов

advertisement
Федеральное агентство по образованию
Уральский государственный технический университет - УПИ
Д.Г. Ермаков
МОДЕЛИРОВАНИЕ ИЕРАРХИЧЕСКИХ ОБЪЕКТОВ
СРЕДСТВАМИ РЕЛЯЦИОННЫХ СУБД
Научный редактор проф., д-р техн. наук Ю.И. Кузякин
Печатается по решению редакционно-издательского совета
УГТУ-УПИ от 18.01.2007 г.
Екатеринбург
УГТУ-УПИ
2007
УДК 004.652(075.8)
ББК 32.973-018.2я73
Е72
Рецензенты:
Институт математики УрО РАН, зав. лаб. компьютерных технологий, с. н. с.
А.М. Устюжанин, к. ф.-м. н..
Ермаков Д.Г.
Е72 Моделирование иерархических объектов средствами реляционных СУБД:
учебное пособие/ Д.Г. Ермаков. Екатеринбург: УГТУ-УПИ, 2007.-132 с.
ISBN
Учебное пособие содержит сведения о способах моделирования
произвольных абстрактных иерархических объектов (деревьев) средствами
реляционных СУБД. Описаны базовые способы отображения абстрактных
иерархических структур в реляционные структуры данных: рекурсивный,
правого и левого коэффициентов, вспомогательной таблицы. Для каждого из
способов рассмотрены возможности выполнения операций выборки поддерева,
нахождения корневых элементов и листьев деревьев, добавления элементов в
иерархии и их удаления. Также рассматриваются два частных случая: иерархия
с ограниченным количеством уровней и иерархия с ограниченным количеством
потомков для всех узлов. Пособие содержит варианты заданий для
самостоятельной работы по данной теме.
Издание предназначено для студентов специальностей 230201 –
Информационные системы и технологии и 080801 – Прикладная информатика в
экономике.
Библиогр.: 13 назв. Табл. 16. Рис. 87.
Подготовлено кафедрой «Анализ систем и принятие решений»
УДК 004.652(075.8)
ББК 32.973-018.2я73
ISBN
© ГОУ ВПО «Уральский государственный
технический университет – УПИ», 2007
© Д.Г. Ермаков 2007 г.
-2-
Оглавление
1. ОСНОВНЫЕ ПОНЯТИЯ И ОПРЕДЕЛЕНИЯ ................................................... 4
1.1.
ИЕРАРХИЧЕСКАЯ МОДЕЛЬ ДАННЫХ ................................................................. 4
1.2.
РЕЛЯЦИОННАЯ МОДЕЛЬ ДАННЫХ .................................................................... 9
1.3.
ЗАДАЧА МОДЕЛИРОВАНИЯ ............................................................................... 9
2. ТРИ БАЗОВЫХ СПОСОБА МОДЕЛИРОВАНИЯ ИЕРАРХИЙ .................. 11
2.1.
РЕКУРСИВНЫЙ СПОСОБ ПРЕДСТАВЛЕНИЯ ИЕРАРХИИ .................................... 11
2.2.
СПОСОБ ПРАВОГО И ЛЕВОГО КОЭФФИЦИЕНТОВ ............................................ 64
2.3.
СПОСОБ ВСПОМОГАТЕЛЬНОЙ ТАБЛИЦЫ ........................................................ 99
3. ДВА ВАЖНЫХ ЧАСТНЫХ СЛУЧАЯ .......................................................... 125
3.1.
СЛУЧАЙ ОГРАНИЧЕННОГО КОЛИЧЕСТВА УРОВНЕЙ ИЕРАРХИИ .................... 125
3.2.
СЛУЧАЙ ОГРАНИЧЕННОГО ЧИСЛА ПОТОМКОВ ............................................. 128
ЗАКЛЮЧЕНИЕ ....................................................................................................... 130
БИБЛИОГРАФИЧЕСКИЙ СПИСОК.................................................................... 136
-3-
1.
Основные понятия и определения
1.1. Иерархическая модель данных
"Дерево" может быть определено как иерархия узлов с попарными
связями, в которой:
 самый
верхний
уровень
иерархии
имеет
единственный
узел,
называемый корнем;
 каждый узел, кроме корня, связан с одним и только одним узлом,
находящимся на более высоком уровне по сравнению с ним самим.
И т.д.
"Дерево" может быть представлено как специальный вид направленного
графа. Графы – структуры данных, состоящие из узлов, связанных дугами.
Каждая дуга показывает однонаправленную связь между двумя узлами.
Вершина графа называется корнем (рис. 1). Остальные элементы называют
узлами иерархии.
Узел, связанный с данным узлом и находящийся на более низком уровне
в иерархии, называют дочерним узлом или потомком данного узла.
Узел, связанный с данным узлом и находящийся на более высоком
уровне в иерархии, называют родительским узлом или предком данного узла.
Потомки, или дети, родительского узла – все узлы в поддереве, имеющие
родительский узел корнем.
Узлы дерева, которые не имеют потомков, называют листьями (рис. 1).
В общем случае, дерево называется n-мерным, если его некоторый узел
может иметь не более чем n узлов - потомков. Все элементы иерархии показаны
на рис. 2 – 5.
-4-
Рис. 1. Основные элементы иерархии
Рис. 2. Уровни иерархии
-5-
Рис. 3. Диаграмма максимального пути
Рис. 4. Глубина пути в иерархии
-6-
Рис. 5. Семейство и размерность семейства иерархии
Рис. 6. Стандартное графическое представление иерархии в ОС MS Windows
Другой способ представления иерархий – вложенные множества. Здесь
корень дерева – это внешнее или объемлющее множество, содержащее все узлы
дерева. Каждый узел в дереве рассматривается как множество своих потомков
(рис. 7).
-7-
Рис. 7. Представление иерархии как набора вложенных множеств
В качестве примеров использования иерархий для моделирования
объектов реального мира можно отметить организационные диаграммы
(графы), генеалогические деревья, карты как описания географических
объектов и т.п.
Для работы с данными этого типа еще в 60-70 гг. прошлого века были
разработаны иерархические СУБД, например IBM IMS, выполняющаяся на
компьютерах IBM с архитектурой System/360, System/370, System/390, System z.
Однако подобные продукты в настоящее время в нашей стране не имеют
широкого
распространения.
Другие
предоставляются XML.
-8-
средства
работы
с
иерархиями
1.2. Реляционная модель данных
Реляционные СУБД создавались таким образом, чтобы пользователи
воспринимали их как совокупность таблиц. Архитектура реляционных баз
данных ориентирована на хранение в отношениях (таблицах) информации о
сущностях и связях между ними. Важной особенностью реляционной модели
является наличие простого и мощного языка запросов SQL.
1.3. Задача моделирования
В
реляционных
СУБД
организация
хранения
информации
о
независимых друг от друга экземплярах сущностей (так называемых «плоских»
данных) не вызывает никаких затруднений. Однако в реляционной модели и
языке SQL нет встроенных средств поддержки иерархических структур данных.
Практически
любая
промышленная
база
данных
должна
обладать
возможностями хранения данных с иерархической организацией. На практике,
при построении информационных систем, приходится обеспечивать хранение в
реляционной БД информации о «вложенных» друг в друга сущностях, т.е.
иерархические данные (самой распространенной иерархической структурой
является организационная структура предприятия, в которой отражаются
отношения
между
различными
подразделениями
и
их
служащими,
представляющиеся при помощи деревьев). Организация хранения такой
информации в реляционных БД не очевидна.
Наиболее часто возникают следующие задачи, характерные только для
иерархий:
 определить, находится ли узел А в поддереве, вершиной которого
является узел Б;
 выбрать непосредственного родителя узла А;
 выбрать всех родителей узла А в порядке их уровня в дереве;
-9-
 выбрать все узлы, находящиеся в поддереве, вершиной которого
является узел А;
 выбрать все узлы, непосредственным родителем которых является узел
А;
 определить наиболее близкого общего родителя для узлов А и B.
Кроме того, существуют более сложные задачи, например задача объединения деревьев, обратная задача – выделение (удаление) поддерева из
иерархии, получение количества всех потомков у данного элемента,
вычисление того, на каком уровне находится некоторый узел, или требуется
получить список всех потомков заданного узла, у которых, в свою очередь, нет
потомков и т.п.
- 10 -
2.
Три базовых способа моделирования иерархий
В реальности каждый элемент иерархии имеет содержательную часть и
должен быть снабжён уникальным идентификатором (вставить пример о
больнице). При изучении способов моделирования иерархических структур
средствами реляционных СУБД мы будем рассматривать абстрактную
иерархию, представленную на рис. 1, где содержимое узла иерархии и его
уникальный идентификатор полагаем совпадающими.
2.1. Рекурсивный способ представления иерархии1
Классически проблема представления иерархий решается с помощью
рекурсивной связи, что позволяет хранить в одной таблице дерево любой
глубины и размерности. Приведём пример такой таблицы для иерархии,
представленной на рис. 8, табл. 1.
Рис. 8. Пример иерархии или дерева
1
Этот метод также называют списком смежности или матрицей смежности
- 11 -
Таблица 1
Реляционная модель иерархии, представленной на рис. 8
Уникальный
идентификатор узла
Указатель на
непосредственного
предка узла
Содержательная
часть
A
B
C
D
E
F
G
H
I
J
K
L
O
M
N
P
A или NULL
A
A
B
B
C
C
C
D
E
E
E
F
G
G
H
A
B
C
D
E
F
G
H
I
J
K
L
O
M
N
P
Таблицу можно создать следующим предложением SQL:
CREATE TABLE T (Id INT NOT NULL IDENTITY PRIMARY KEY,
Parent INT NOT NULL REFERENCES T(Id),
Data VARCHAR);
Здесь создаётся таблица с именем T и тремя столбцами Id, Parent и Data
(рис. 9).
- 12 -
Рис. 9. Просмотр созданного отношения в среде MS SQL Server Management
Studio Express.
Столбец с именем Id должен содержать уникальные идентификаторы
текущих узлов. В примере для него определён целочисленный тип данных с
автоматическим наращиванием, хотя можно использовать и любой другой,
наилучшим образом соответствующий описываемой предметной области. Для
создания искусственных уникальных идентификаторов во многих диалектах
SQL имеется специальный тип данных uniqueidentifier. Столбец Id не
должен иметь неопределённых значений. Кроме того, он объявляется
первичным ключом.
Parent
–
это
столбец,
значения
которого
указывают
на
непосредственного предка текущего элемента. Тип данных этого столбца
должен соответствовать типу данных столбца Id. В столбце Parent могут
находиться только те значения, которые уже имеются в столбце Id, т.е.
значения в этом поле являются ссылками (указателями) на поле Id – внешним
ключом при замыкании таблицы на себя. Наличие неопределённых значений в
этом столбце запрещено.
При решении практических задач данная таблица должна быть снабжена
столбцами для хранения содержательной части узлов дерева. В приведённом
выше предложении SQL, создающим таблицу, – это столбец с именем Data.
Заполним таблицу данными в соответствии с рис. 8, с помощью следующих
предложений SQL:
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
T
T
T
T
T
T
T
T
T
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
Data)
Data)
Data)
Data)
Data)
Data)
Data)
Data)
Data)
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
- 13 -
(1,
(1,
(1,
(2,
(2,
(3,
(3,
(3,
(4,
'A');
'B');
'C');
'D');
'E');
'F');
'G');
'H');
'I');
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INTO
INTO
INTO
INTO
INTO
INTO
INTO
T
T
T
T
T
T
T
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
(Parent,
Data)
Data)
Data)
Data)
Data)
Data)
Data)
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
(5,
(5,
(5,
(6,
(7,
(7,
(8,
'J');
'K');
'L');
'O');
'M');
'N');
'P');
После заполнения данными из примера на рис. 8, таблица получает
следующий вид (рис. 10).
Рис. 10. Таблица заполнена данными
Обратите внимание. Корневой элемент иерархии не может иметь
предка по определению. Таблица, полученная в результате выполнения нашего
SQL предложения, не допускает неопределённых значений ни в одном из
столбцов. Поэтому, при добавлении в таблицу соответствующей записи, для
корневого элемента дерева следует указать в качестве непосредственного
предка идентификатор самого корневого элемента.
- 14 -
Не рекомендуется создавать корневой элемент с неопределённым
значением для поля указателя на предка, а затем накладывать ограничение
FOREIGN KEY, так как восстановление такой базы данных из резервной копии
будет невозможно (попробуйте самостоятельно ответить на вопрос – почему в
этом случае будет невозможно восстановление базы из резервной копии).
Чтобы получить из таблицы T все корневые элементы, достаточно
выполнить запрос:
SELECT * FROM T WHERE Parent=Id;
Результат выполнения запроса представлен на рис. 11.
Рис. 11 Результат выполнения запроса на получение корневого элемента
иерархии
В вариантном исполнении ограничение на то, что поле, указывающее
непосредственного предка, не может содержать неопределённые значения,
- 15 -
может быть снято. Тогда наличие неопределённого значения может быть
использовано как признак корня дерева.
SELECT * FROM T WHERE Parent=NULL;
В общем случае (кроме случая совпадения идентификатора узла и
идентификатора его предка) в качестве признака того, что элемент является
корневым, может быть любое значение столбца Parent, которое не
используется в качестве уникального идентификатора узла.
Идентификаторы узлов можно получить следующим предложением SQL:
SELECT Id FROM T;
В этом примере результат этого запроса будет следующий (рис. 12).
- 16 -
Рис. 12. Список идентификаторов всех узлов иерархии
Если в таблице хранится несколько независимых иерархий, все
корневые элементы можно определить таким образом:
SELECT * FROM T WHERE Parent NOT IN (SELECT Id FROM T);
Далее, если не оговорено иное, будем полагать, что структуры данных
используются для представления единственной иерархии.
Обратите внимание. Столбец «Указатель на непосредственного предка узла»
содержит список всех узлов дерева, имеющих потомков, т.е. не являющихся
листьями.
Если идентификатор узла не содержится в столбце «Указатель на
непосредственного предка узла», это значит, что данный узел не имеет
потомков (данный узел является листом дерева). Когда в таблице хранится
единственная иерархия, для того, чтобы получить список всех листьев, нужно
выбрать те значения из столбца «Уникальный идентификатор узла», которые
отсутствуют в столбце «Указатель на непосредственного предка узла».
Для того что бы выбрать все узлы (без повторов), имеющие потомков,
выполняется следующее SQL-предложение (рис. 13):
SELECT DISTINCT Parent FROM T;
- 17 -
Рис. 13. Список всех узлов, имеющих потомков
Тогда список узлов, не имеющих потомков, получаем как выборку имён узлов,
не входящих в список узлов, являющихся предками других узлов (рис. 14):
SELECT DISTINCT Id, Data FROM T
WHERE Id NOT IN (SELECT DISTINCT Parent FROM T);
- 18 -
Рис. 14 Список листьев (список узлов, не имеющих потомков)
Другой вариант запроса с таким же результатом, но с использованием EXIST
(рис. 15):
SELECT * FROM T AS E1 WHERE NOT EXISTS
(SELECT * FROM T AS E2 WHERE E1.Id=E2.Parent);
- 19 -
Рис. 15. Результат выполнения запроса на поиск листьев с использованием
оператора EXIST
Данная структура является минимально необходимой и достаточной для
организации иерархии в реляционной СУБД. Её называют структурой со
ссылкой на предка. Пользуясь такой таблицей, легко можно найти родителя или
потомка некоторого произвольного элемента.
Для того чтобы получить всех потомков, например узла E, нужно
использовать его уникальный идентификатор - Id в том же самом запросе как
идентификатор родителя.
Получаем идентификатор узла E (рис.16):
SELECT Id FROM T WHERE Data='E';
- 20 -
Рис. 16. Результат выборки идентификатора узла E
Далее
выбираем
все
узлы,
для
которых
идентификатор
непосредственного предка равен идентификатору узла E:
SELECT * FROM T WHERE Parent=5;
Результат этого запроса будет выглядеть следующим образом (рис. 17).
- 21 -
Рис. 17. Список узлов, являющихся непосредственными потомками узла E
Объединив эти два предложения SQL, получаем (рис. 18)
SELECT * FROM T WHERE
Parent=(SELECT Id FROM T WHERE Data='E');
- 22 -
Рис. 18. Выбираем всех потомков узла E единым SQL-предложением
Аналогично определяется непосредственный предок заданного узла,
например узла E:
SELECT * FROM T WHERE
Id=(SELECT Parent FROM T WHERE Data='E');
Результат выполнения такого запроса представлен на рис. 19.
- 23 -
Рис. 19. Непосредственный предок узла E
Для того чтобы вывести два уровня в дереве (все пары
непосредственный предок – непосредственный потомок), необходимо написать
более сложный запрос (рис. 20):
SELECT T1.Data, T2.Data
FROM T AS T1, T AS T2, T AS T3
WHERE T1.Id=T2.Parent AND T3.Id=T2.Parent;
- 24 -
Рис. 20. Получаем два уровня иерархии (набор всех пар – непосредственный
предок – непосредственный потомок)
Для того чтобы выбрать значения более чем на два уровня глубже в
иерархии, просто расширяем предыдущий запрос (рис. 21):
SELECT DISTINCT T1.Data, T2.Data, T3.Data
FROM T AS T1, T AS T2, T AS T3, T AS T4
WHERE T1.Id=T2.Parent AND
T2.Id=T3.Parent AND
T3.Id=T4.Parent;
- 25 -
Рис. 21. Три уровня иерархии
К сожалению, в общем случае неизвестно, насколько глубоко дерево, так
что для нахождения полного пути в дереве нужно расширять этот запрос, пока
не получится в результате пустое множество.
Для выполнения выборок потомков некоторого заданного узла или его
предков используются рекурсивные запросы.
Выборка поддерева по заданному узлу средствами MS SQL Server будет
иметь следующий вид:
WITH SubTree ([Id], [Parent], [Data], [Level]) AS
(
-- Задаем начальное значение для счетчика уровней 1
SELECT [Id], [Parent], [Data], 1 FROM T
-- Задаем корень искомого поддерева (Parent=1)
WHERE [Parent]=1
UNION ALL
-- Выполняем рекурсивную выборку с
-- наращиванием счетчика уровней
- 26 -
SELECT T.[Id], T.[Parent], T.[Data], [Level]+1
FROM T
INNER JOIN SubTree ON T.[Parent]=SubTree.[Id]
-- исключаем корень
WHERE SubTree.[Parent]<>SubTree.[Id]
)
SELECT [Id], [Parent], [Data], [Level] FROM SubTree;
- 27 -
Выборка всех предков для заданного узла (путь к узлу от корня)
выполняется аналогично:
WITH SubTree ([Id], [Parent], [Data], [Level]) AS
(
SELECT [Id], [Parent], [Data], 1 FROM T
WHERE [Id]=4 -- Уникальный идентификатор узла
UNION ALL
SELECT T.[Id], T.[Parent], T.[Data], Level+1 FROM T
INNER JOIN SubTree ON T.[Id]=SubTree.[Parent] AND
SubTree.[Parent]<>SubTree.[Id]
)
SELECT [Id], [Parent], [Data],
(SELECT MAX(Level) FROM SubTree) AS [Level]
FROM SubTree;
- 28 -
Проверка на вхождение узла в поддерево, определяемое заданным
корневым элементом, может быть выполнена следующим запросом:
WITH SubTree([Id], [Parent], [Data], [Level]) AS
(
SELECT [Id], [Parent], [Data], 1 FROM T
WHERE [Id]=6 -- проверяемый узел
-- попробуйте изменять это значение
UNION ALL
SELECT T.[Id], T.[Parent], T.[Data], [Level]+1 FROM
T
INNER JOIN SubTree ON T.[Id]=SubTree.[Parent] AND
SubTree.[Parent]<>SubTree.[Id]
)
SELECT result=
CASE WHEN EXISTS
(
SELECT 1 FROM SubTree
WHERE [Id]=3 -- корень поддерева
-- попробуйте изменять это значение
)
THEN 'Узел входит в поддерево'
ELSE 'Узел НЕ входит в поддерево'
END;
- 29 -
- 30 -
- 31 -
Если СУБД не умеет выполнять такие запросы, то для получения
аналогичного результата придется использовать другие механизмы, например,
временные таблицы и хранимые процедуры и функции.
Сначала рассмотрим более простую задачу – поиск всех предков для
некоторого, наперёд заданного узла. Для этого по заданному идентификатору
узла
выбираем
идентификатор
его
непосредственного
предка,
по
идентификатору предка – предка предка и т.д., пока не достигнем корня,
который в нашем случае определяется как узел, указывающий как на предка на
самого себя. Приведём пример создания такой функции.
CREATE FUNCTION GetParents
(@NodeName AS varchar(1))
RETURNS @tree table
(
Name VARCHAR(1) NOT NULL,
Level int NOT NULL
)
AS
BEGIN
DECLARE @ID int;
DECLARE @Parent int;
DECLARE @Level int;
-----
ВНИМАНИЕ!
После каждой операции необходима
проверка наличия ошибок,
отсутствующая в данном примере!
-----
По имени узла получаем его идентификатор и
идентификатор его непосредственного предка.
Если б не требовалась организация цикла,
можно было бы обойтись только идентификатором предка.
SELECT @ID=ID, @Parent=Parent
FROM T
Where Data=@NodeName;
-- Для самого узла указываем нулевое расстояние до предка
SET @Level=0;
-- Добавляем запись в возвращаемую таблицу
- 32 -
INSERT INTO @tree (Name, Level) VALUES (@NodeName,
@Level);
-- Пока не доберёмся до корневого элемента иерархии...
WHILE (@ID <> @Parent)
BEGIN
SELECT @ID=ID, @Parent=Parent, @NodeName=Data
FROM T
WHERE ID=@Parent;
-- Наращиваем расстояние до предка
SET @Level=@Level+1;
-- Добавляем запись в возвращаемую таблицу
INSERT INTO @tree (Name, Level) VALUES
(@NodeName, @Level);
END
RETURN
END
При помощи этой функции получим всех предков, например, узла O:
select * from GetParents('O') ORDER BY Level DESC;
Результат выполнения этого запроса представлен на рис. 22.
- 33 -
Рис. 22. Использование функции нахождения всех предков некоторого узла для
узла O
Теперь при помощи функции GetParents можно решить задачу о
нахождении ближайшего общего предка для двух узлов. Пусть заданы узлы I и
K. Их общий ближайший предок – узел B (см. рис. 8).
С помощью запросов
select [name] from GetParents('I');
и
select [name] from GetParents('K');
получим списки всех предков этих узлов (рис. 23).
Рис. 23 Все предки узлов I и K
- 34 -
Запрос
select * from GetParents('I') where
([name] in (select [name] from GetParents('K')));
даст список всех общих предков для узлов I и K (рис. 24).
Рис. 24. Список всех общих предков для узлов I и K
Из этого списка надо выбрать элемент с минимальным значением поля
Level (рис. 25).
select MIN([level]) from GetParents('I') where
[name] in (select [name] from GetParents('K'));
- 35 -
Рис. 25. Получаем элемент с минимальным значением в поле Level
Окончательно получаем ближайшего общего предка для узлов I и K при
помощи следующего предложения SQL (рис. 26):
select [name] from GetParents('I') where
([name] in (select [name] from GetParents('K'))
And ([level]=((
select MIN([level]) from GetParents('I') where
[name] in (select [name] from GetParents('K'))))));
Рис. 26. Получаем ближайшего общего предка для узлов I и K
- 36 -
Задача выбора всех потомков (или поддерева) для некоторого, наперёд
заданного узла более сложная, так как, в отличие от предков, которые для
каждого узла единственные на каждом уровне, у одного предка может быть
несколько различных непосредственных потомков.
Для решения этой задачи можно использовать средства некоторого
внешнего объемлющего языка уровня приложения или хранимые процедуры
СУБД. Здесь может использоваться рекурсивный вызов, однако следует учесть,
что существует ограничение на вложенность рекурсивных вызовов (например,
не более 100 для MS SQL Server 2005 Express Edition), а дерево может быть
больше, чем разрешённая глубина вложенности рекурсивных вызовов.
Приведём пример рекурсивной функции, использующей курсор:
CREATE FUNCTION MyGetTree(@root varchar(1),
@order varchar(4096),
@level int)
RETURNS @tree TABLE
(
[name] varchar,
id int,
parent int,
[level] int,
[order] varchar(4096)
)
AS
Begin
-----
ВНИМАНИЕ! В реальной функции необходимо
проверить законность входных параметров.
После каждой операции необходима проверка
наличия ошибок, отсутствующая в данном примере!
DECLARE @id int;
DECLARE @parent int;
DECLARE @data varchar(1);
IF @level=0
BEGIN
- 37 -
-- Если это корневой элемент
-- записываем в таблицу информацию о
-- заданном узле выбираемого поддерева.
insert @tree ([name], id, parent, [level], [order])
select Data, Id, Parent, 0, Data from T where
Data=@root;
-- Заполняем поле для упорядочивания
SET @order=@root;
END -- IF
ELSE
BEGIN
SET @order=@order+'::'+@root;
select @id=Id from T where Data=@root;
END
-- Узнаём уникальный идентификатор обрабатываемого узла
select @id=Id from T where Data=@root;
-- Выбираем непосредственных потомков
-- Объявляем курсор
declare c1 cursor
keyset
for select Id, Parent, Data FROM T WHERE Parent=@Id;
-- Открыли курсор
open c1
-- Выбрали первого потомка
fetch next from c1 into @id, @parent, @data;
while (@@fetch_status <> -1)
-- Пока не закончились непосредственные потомки...
begin
insert @tree ([name], id, parent, [level], [order])
VALUES
(@data, @id, @parent, @level+1, @order+'::'+@data);
-- Рекурсивный вызов функции
insert @tree ([name], id, parent, [level], [order])
SELECT * FROM MyGetTree(@data, @order, @level+1);
-- Очередной потомок
fetch next from c1 into @id, @parent, @data;
end
deallocate c1;
- 38 -
return
end
При помощи этой функции получим поддерево, начинающееся с узла B:
select * from MyGetTree('B', '', 0) ORDER BY [order];
Результат выполнения этого запроса представлен на рис. 27.
Рис. 27. Поддерево, начинающееся с узла B
Использование курсоров существенно снижает производительность.
Более универсальным будет вариант, использующий цикл (любую рекурсию
можно превратить в итерацию и наоборот). Приведём пример функции,
основанной на использовании вместо рекурсии итерации:
CREATE FUNCTION MyGetTree(@root varchar(1))
RETURNS @tree TABLE
(
[name] varchar,
id int,
parent int,
- 39 -
[level] int,
[order] varchar(4096)
)
AS
BEGIN
------
ВНИМАНИЕ!
В реальной функции необходимо проверить законность
входных параметров.
После каждой операции необходима проверка
наличия ошибок, отсутствующая в данном примере!
declare @level int set @level=0;
-- Записываем в таблицу информацию о
-- корневом узле выбираемого поддерева.
insert @tree ([name], id, parent, [level], [order])
select Data, Id, Parent, @level, Data from T where
Data=@root;
while 1=1
begin
insert @tree ([name], id, parent, [level],
[order])
select T.Data, T.ID, T.Parent, @level+1,
R.[order]+'::'+T.Data
from T
join @tree AS R on R.id=T.Parent and R.[level]=@level
and
T.Id<>T.Parent;
if @@rowcount=0 break;
select @level=@level+1;
end -- while
return
END
С помощью этой функции можно получить другое описание иерархии,
например, с помощью оператора SELECT (рис. 28):
select * from MyGetTree('A') ORDER BY [order];
- 40 -
Рис. 28. Получаем полное описание иерархии
Другое, почти визуальное представление иерархии можно получить при
помощи этой функции и следующего запроса (рис. 29):
SELECT REPLICATE (' |----- ', level) + [name] AS Tree
FROM MyGetTree('A') ORDER BY [order];
- 41 -
Рис. 29. Визуализация иерархии средствами SQL
Сравните полученный результат с представлением иерархии на рис. 6.
Обратите внимание. Между уровнем модели данных и уровнем
визуального представления данных в приложении нет жёсткой связи. Не
стремитесь к тому, чтобы структура данных каким-то образом полностью
соответствовала интерфейсу программы. Обычно это тупиковый путь.
Важно правильно спроектировать структуры данных, а как данные
отображать – задача конкретного приложения. Одни и те же данные,
хранимые в базе, могут использоваться различными приложениями с
существенно различающимися интерфейсами. Структура базы и визуальный
интерфейс единственно не должны друг другу противоречить.
- 42 -
На практике встречаются задачи, требующие упорядочения узлов одного
предка и находящихся на одном уровне. Для решения таких задач расширим
базовую таблицу, добавив столбец ORDER. Значения этого столбца задают
порядок следования элементов с общим родителем в пределах одного уровня.
Для случая нескольких вариантов сортировок можно ввести несколько
аналогичных столбцов. Другим, неявным, способом задания номера потомка
может быть установка ссылки, например, на «предыдущего брата».
Добавление и удаление листьев в дереве в рассматриваемой модели
трудности не представляет. Проделайте эти операции самостоятельно.
Добавление поддерева сводится к последовательному добавлению
листьев.
Удаление поддерева может быть реализовано на основе функции для
получения узлов поддерева. Удалим, например, поддерево с корнем в узле H
следующим образом:
DELETE FROM T WHERE Id IN (
SELECT Id FROM MyGetTree('H'));
и проверим результат выполнения запроса, выведя всё дерево, начиная с корня.
- 43 -
Рис. 30. Проверка выполнения операции удаления поддерева
Сравните результаты выполнения запроса, представленные на рис. 29 и рис. 30.
Более сложная задача – удаление единственного узла, имеющего
потомков. При удалении такого узла требуется принять решение, какой элемент
нужно назначить корневым для «отрывающегося» набора узлов (обычно
решение этого вопроса определяется из свойств предметной области).
Например, при удалении узла E узлы J, K, L лишаются корневого элемента
поддерева и «отрываются» от иерархии (рис. 31).
- 44 -
Рис. 31. Последствия удаления узла, имеющего потомков
Чаще всего для каждого непосредственного потомка удаляемого узла,
остающегося в составе структуры иерархии, надо изменить значение указателя
на удаляемого предка на указатель на непосредственного предка этого предка.
Например, при исключении узла E, значение предка для узлов J, K, L должно
быть установлено таким образом, чтобы оно указывало на узел B (рис. 32).
- 45 -
Рис. 32. Присоединение «оторвавшихся» узлов к ближайшему предку
После этого запись для узла E может быть удалена из таблицы.
Реализуем эти действия при помощи хранимой процедуры:
CREATE PROCEDURE DelNode (@Node VARCHAR) AS
BEGIN
-- ВНИМАНИЕ! В реальной процедуре нужно проверить
-- корректность переданных при вызове параметров.
-- После каждой операции необходима проверка наличия
-- ошибок, отсутствующая в данном примере!
DECLARE @Parent int;
DECLARE @Id
int;
-- По имени получаем уникальный идентификатор
-- удаляемого узла и
-- уникальный идентификатор его непосредственного предка.
SELECT @Id=Id, @Parent=Parent FROM T Where Data=@Node;
-- Заменяем указатель на родительский элемент
-- (переподчиняем потомков удаляемого узла
-- его родительскому узлу)
UPDATE T SET Parent=@Parent WHERE Id IN (
SELECT Id FROM T WHERE Parent=@Id);
- 46 -
-- Удаляем узел
DELETE FROM T WHERE Id=@Id;
END
Теперь проверим работу хранимой процедуры удалением узла E с
последующим выводом структуры всего дерева.
EXEC DelNode 'E';
SELECT REPLICATE (' |----- ', level) + [name] AS Tree
FROM MyGetTree('A') ORDER BY [order];
Результат этих операций представлен на рис. 33.
Рис. 33. Удаление единственного узла
- 47 -
Аналогично решается задача включения нового узла внутрь иерархии
(рис. 34).
Рис. 34 Включение нового узла в иерархию
Для этого выполняем следующие действия:
1) добавляем новый узел как лист;
2) присоединяем к нему узлы – потомки, заменяя связи узлов,
становящихся потомками вновь добавленного узла с их старым
корнем.
В данной структуре информация о полном пути и уровне элемента в
иерархии явно нигде не хранится. Из-за этого возникают сложности при
формировании полного пути в дереве и вычислении уровня в иерархии, на
котором находится элемент. Для решения этой проблемы можно дополнить
структуру таблицы столбцом, в котором будет храниться значение уровня
текущего элемента в дереве. Описание таблицы тогда будет выглядеть
следующим образом:
- 48 -
Таблица 2
Усовершенствованная модель иерархии
Уникальный
Указатель на
Уровень узла
идентификатор
непосредственного
в иерархии
узла
предка узла
A
A или NULL
1
B
A
2
C
A
2
D
B
3
E
F
G
H
I
J
K
L
O
M
N
P
B
C
C
C
D
E
E
E
F
G
G
H
3
3
3
3
4
4
4
4
4
4
4
4
Содержательная
часть
A
B
C
D
E
F
G
H
I
J
K
L
O
M
N
P
Таблица создаётся следующим SQL-предложением:
create table T (Id int not null identity primary key,
Parent int not null references T(Id),
[Level] int default 1 not null,
Data varchar);
В столбце Level хранятся числовые значения уровней узлов в дереве.
По умолчанию значение уровня устанавливается равным 1. Поддержка
корректности значений, вводимых в этот столбец, возлагается на триггер,
назначаемый операции добавления записей в таблицу (INSERT) или, если
триггер создать невозможно, на приложение. Для заполнения этого поля по
идентификатору родительского элемента нужно определить его уровень и,
нарастив на единицу, установить полученное значение для добавляемого узла.
CREATE TRIGGER AddNode
- 49 -
ON T
FOR INSERT
AS
BEGIN
-- ВНИМАНИЕ! В реальной процедуре нужно проверить
-- корректность переданных при вызове параметров.
-- После каждой операции необходима проверка наличия
-- ошибок, отсутствующая в данном примере!
DECLARE @Parent int;
DECLARE @Id
int;
DECLARE @Level int;
-- Получаем вновь вставленную запись.
-- ВНИМАНИЕ! В реальном триггере нужно учесть,
-- что может быть добавлено несколько записей.
SELECT @Id=Id, @Parent=Parent, @Level=[Level] FROM
inserted;
-----
ВНИМАНИЕ! Идентификатор родительского узла
может быть не указан.
В реальном триггере следует предусмотреть
обработку этой ситуации.
-- Узнаём значение уровня родительского узла
SELECT @Level=[Level] FROM T WHERE Id=@Parent;
-- Устанавливаем правильное значение для уровня узла
UPDATE T SET [Level]=@Level+1 WHERE Id=@Id;
END
Проверим работу триггера добавлением записи в таблицу
INSERT INTO T(Parent, Data) VALUES(1, 'B');
и выбрав значение в столбце Level, соответствующее добавленному узлу:
SELECT [Level] FROM T WHERE Data='B';
Результат операции представлен на рис. 35.
- 50 -
Рис. 35. Уровень узла в иерархии
В данной структуре присутствует, как минимум, один возможно
опасный недостаток – отсутствие контроля правильности ссылки на родителя.
Например, если в качестве предка узла B по какой-то причине указан узел K,
получается петля, которая породит бесконечный цикл (табл. 7 и рис. 7).
Проверку на существование петель можно осуществить, например написав
соответствующую функцию, на основе функции для определения всех предков
заданного узла.
CREATE FUNCTION MyCheckLoop (@NodeName varchar(1))
RETURNS @tree table
(
[Name] VARCHAR(1)
NOT NULL,
ID
int
NOT NULL,
Parent int
NOT NULL,
[Level] int
NOT NULL,
[Check] int
NOT NULL,
[Path] VARCHAR(4096) NOT NULL
)
AS
BEGIN
DECLARE @ID int;
DECLARE @Parent int;
DECLARE @Level int;
DECLARE @Check int;
DECLARE @Path VARCHAR(4096);
- 51 -
-- ВНИМАНИЕ! После каждой операции необходима
-- проверка наличия ошибок, отсутствующая
-- в данном примере!
-- По имени узла получаем его идентификатор и
-- идентификатор его непосредственного предка.
SELECT @ID=ID, @Parent=Parent
FROM T
Where Data=@NodeName;
-- Для самого узла указываем нулевое расстояние до предка
SET @Level=0;
-- Пока петель не найдено
SET @Check=0;
-- Путь в дереве
SET @Path=@NodeName;
-- Добавляем запись в возвращаемую таблицу
INSERT INTO @tree ([Name], ID, Parent, [Level],
[Check], [Path])
VALUES (@NodeName, @ID, @Parent, @Level,
@Check, @Path);
-- Пока не доберёмся до корневого элемента иерархии
-- или не обнаружим повтор
WHILE (@ID <> @Parent)
BEGIN
SELECT @ID=ID, @Parent=Parent, @NodeName=Data
FROM T
WHERE ID=@Parent;
-- Наращиваем расстояние до предка
SET @Level=@Level+1;
-- Добавляем элемент пути
SET @Path=@Path+'::'+@NodeName;
-- Проверяем, нет ли уже записи об этом узле
-- в результирующей таблице
select @Check=COUNT([Check]) from @tree
where [Name]=@NodeName and
ID=@ID and
- 52 -
Parent=@Parent and
[Level]<>@Level;
-- Добавляем запись в возвращаемую таблицу
INSERT INTO @tree ([Name],
ID,
Parent,
[Level],
[Check],
[Path])
VALUES (@NodeName,
@ID,
@Parent,
@Level,
@Check,
@Path);
-- Если обнаружена петля выходим из
-- цикла и завершаем работу функции
if @Check<>0 BREAK
END -- while
RETURN
END
На целой ветви от узла K до корня результат будет следующим (рис. 36)
SELECT * FROM MyCheckLoop('K');
- 53 -
Рис. 36. Проверка целостности структуры дерева – петель нет
Теперь испортим тестовую иерархию, создав петлю, как это показано в
табл. 3 и на рис. 37-39.
Таблица 3
Пример образования петли
Уникальный
Указатель на
Содержательная
идентификатор непосредственного
часть
узла
предка узла
A
A или NULL
A
B
B
K
C
A
C
D
B
D
E
E
B
F
C
F
G
C
G
H
C
H
I
D
I
J
E
J
K
K
E
L
E
L
- 54 -
Уникальный
Указатель на
Содержательная
идентификатор непосредственного
часть
узла
предка узла
O
F
O
M
G
M
N
G
N
P
H
P
Рис. 37. Пример образования петли
- 55 -
Рис. 38. Создание петли, шаг 1
- 56 -
Рис. 39. Создание петли, шаг 2
В этом случае результат проверки будет таким, как показано на рис. 40.
- 57 -
Рис. 40. Обнаружение наличия петли в структуре иерархии
Наличие ненулевого значения в столбце Check свидетельствует о наличии
петли в структуре данных. Значения в полях Path, ID и Parent позволяют
локализовать и исправить ошибку.
Для того чтобы проверить целостность всего дерева, достаточно
проверить отсутствие петель по всем путям от корня до листьев. Для этого
строим список листьев и проверяем функцией MyCheckLoop каждый элемент
этого списка.
CREATE FUNCTION CheckLoopTree()
RETURNS @tree table
(
[Name] VARCHAR(1)
NOT
ID
int
NOT
Parent int
NOT
[Level] int
NOT
[Check] int
NOT
[Path] VARCHAR(4096) NOT
)
AS
- 58 -
NULL,
NULL,
NULL,
NULL,
NULL,
NULL
BEGIN
DECLARE @Data varchar(1);
-- ВНИМАНИЕ! После каждой операции необходима
-- проверка наличия ошибок, отсутствующая в данном
примере!
-- Последовательно выбираем все листья
-- Для этого создаём курсор
declare c1 cursor
keyset
for
SELECT DISTINCT Data FROM T WHERE
Id NOT IN (SELECT DISTINCT Parent FROM T);
-- Открыли курсор
open c1
-- Выбрали первый лист
fetch next from c1 into @data;
while (@@fetch_status <> -1)
-- Пока не перебрали все листья...
begin
INSERT INTO @tree ([Name],
ID,
Parent,
[Level],
[Check],
[Path])
SELECT [Name],
ID,
Parent,
[Level],
[Check],
[Path]
FROM MyCheckLoop(@data);
-- Следующий лист...
fetch next from c1 into @data;
end
deallocate c1;
return
end
- 59 -
Выполним эту функцию для дерева без петель:
SELECT * FROM CheckLoopTree();
Результат выполнения представлен на рис. 41.
Теперь снова, как в предыдущем примере, испортим тестовую иерархию,
создав петлю, и повторим проверку (рис. 42):
SELECT * FROM CheckLoopTree();
- 60 -
Рис. 41. Проверка целостности дерева, петли отсутствуют
- 61 -
Рис. 42. Проверка целостности дерева, обнаружены петли
- 62 -
Ненулевые значения в столбце Check свидетельствует о наличии, по крайней
мере, одной петли в структуре данных (см. рис. 42). Значения в полях Path, ID и
Parent позволяют локализовать и исправить ошибку.
Действительно ли это недостаток подхода? Благодаря этому свойству в
таблице можно хранить несколько различных иерархий и даже более сложные
сетевые структуры, допускающие для каждого узла более одного предка.
Задачи
Создайте таблицу, описывающую абстрактную иерархию со структурой
со ссылкой на предка. Заполните её данными. На основе этой таблицы
сформируйте и выполните SQL-предложения, решающие следующие задачи:
1.
Определить корневой элемент иерархии.
2.
Выбрать непосредственных потомков для некоторого заданного
элемента.
3.
Определить непосредственного предка некоторого заданного
элемента.
4.
Добавить в иерархию новый элемент как потомка заданного узла.
5.
Определить, что заданный элемент не имеет потомков.
6.
Выбрать все элементы иерархии, не имеющие потомков (листья).
Задачи повышенной сложности
1. Определить уровень в иерархии одного заданного элемента
относительно другого заданного элемента.
2. Определить ближайшего общего предка для двух наперёд заданных
узлов.
3. Добавить в иерархию новый элемент как потомка заданного узла с
автоматическим заполнением поля уровня в иерархии.
4. Написать хранимую процедуру, извлекающую всё поддерево,
начиная с заданного узла.
- 63 -
5. Удалить заданный элемент иерархии. Учесть возможность наличия
потомков удаляемого элемента.
6. Написать хранимую процедуру, проверяющую структуру на
отсутствие петель.
7. Проверить все узлы иерархии на наличие нескольких предков.
2.2. Способ правого и левого коэффициентов
Метод,
предложенный
Джо
Селко,
называемый
ещё
методом
вложенных множеств, основан на полном обходе дерева (рис. 43). При полном
обходе дерева каждому узлу назначается пара значений – левый и правый
коэффициенты. Левые коэффициенты присваиваются во время движения от
предка к потомку. Правые коэффициенты назначаются при движении от
потомка к предку.
Рис. 43. Назначение коэффициентов при выполнении полного обхода дерева
Корень всегда имеет левый коэффициент, равный 1. Разность между
значениями левого и правого коэффициентов для листьев всегда равна 1.
Правый коэффициент для корня равен удвоенному числу узлов в иерархии (2n),
- 64 -
так как при обходе мы должны посетить каждый узел дважды, один раз с левой
стороны и один раз с правой стороны.
Отношение, соответствующее иерархии из нашего примера (см. рис. 43),
выглядит следующим образом (Табл. 4).
Таблица 4
Модель иерархии на основе метода правого и левого коэффициентов
Значения левого
коэффициента
1
2
3
4
5
8
9
14
15
16
17
20
21
Содержательная часть
данных узла
A
B
D
F
J
G
K
C
E
H
L
I
M
Значения правого
коэффициента
26
13
12
7
6
11
10
25
23
19
18
23
22
Организуемая таким образом структура данных может быть описана
отношением, создаваемым следующим оператором SQL (рис. 44):
create table T (L int not null primary key,
Node varchar,
R int not null);
- 65 -
Рис. 44. Реализация модели иерархии методом правого и левого коэффициентов
Здесь L – это столбец левых коэффициентов, R – правых коэффициентов, а
Node – содержательная информация об узле (здесь может располагаться
несколько столбцов, хранящих содержательную часть узла). Роль уникального
идентификатора узла исполняет левый коэффициент, объявленный первичным
ключом. Также в качестве уникального идентификатора может быть выбран
правый коэффициент или пара – левый и правый коэффициенты, взятые вместе.
Заполним созданную таблицу данными (рис. 45), соответствующими
иерархии представленной на рис. 43, при помощи следующих предложений
SQL:
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INTO
INTO
INTO
INTO
INTO
INTO
INTO
T
T
T
T
T
T
T
(L,
(L,
(L,
(L,
(L,
(L,
(L,
Node,
Node,
Node,
Node,
Node,
Node,
Node,
R)
R)
R)
R)
R)
R)
R)
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
- 66 -
(
(
(
(
(
(
(
1,
2,
3,
4,
5,
8,
9,
'A',
'B',
'D',
'F',
'J',
'G',
'K',
26);
13);
12);
7);
6);
11);
10);
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INTO
INTO
INTO
INTO
INTO
INTO
T
T
T
T
T
T
(L,
(L,
(L,
(L,
(L,
(L,
Node,
Node,
Node,
Node,
Node,
Node,
R)
R)
R)
R)
R)
R)
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
(14,
(15,
(16,
(17,
(20,
(21,
'C',
'E',
'H',
'L',
'I',
'M',
25);
23);
19);
18);
23);
22);
Рис. 45. Модель иерархии, представленной на рис. 8
При такой организации структуры данных для каждого элемента
значения левого и правого коэффициентов его потомков заключены в
интервале, определяемом значениями левого и правого коэффициентов
родителя. Аналогично все родители некоторого элемента имеют значения
левого коэффициента, меньшие значений левого коэффициента потомка и
правого, большие правого коэффициента потомка. Отношения предок –
- 67 -
потомок определяются из того, что значение L потомка всегда больше чем у
предка, а R – меньше (рис. 46).
Рис. 46. Вложенность интервалов, определяемая значениями левых и правых
коэффициентов узлов иерархии
Выбираем всех потомков для узла D (рис. 47):
SELECT * FROM T WHERE
L>(SELECT L FROM T WHERE Node='D') AND
R<(SELECT R FROM T WHERE Node='D');
Рис. 47. Все потомки узла D
Получим всех предков узла D (рис. 48):
- 68 -
SELECT * FROM T WHERE
L<(SELECT L FROM T WHERE Node='D') AND
R>(SELECT R FROM T WHERE Node='D');
Рис. 48. Все предки узла D
Теперь воспользуемся предикатами BETWEEN, чтобы определить всех
предков определенного узла. Для этого необходимо написать следующее
предложение SQL (рис. 49):
SELECT 'D' AS Base,
B1.Node AS Parent,
(B1.R-B1.L) AS height
FROM T AS B1, T AS E1
WHERE E1.L BETWEEN B1.L AND B1.R AND
E1.R BETWEEN B1.L AND B1.R AND
E1.Node='D';
Чем больше значение height, тем дальше по иерархии узлы друг от друга.
- 69 -
Рис. 49. Использование BETWEEN для нахождения всех предков узла D
Уровень в иерархии заданного элемента можно узнать, получив
количество его предков и добавив 1.
Получим уровень в иерархии для узла D (рис. 50):
SELECT COUNT(Node)+1 AS Level FROM T WHERE
L<(SELECT L FROM T WHERE Node='D') AND
R>(SELECT R FROM T WHERE Node='D');
- 70 -
Рис. 50. Уровень узла D в иерархии
Уровень узла в иерархии, можно получить, используя предикат
BETWEEN следующим образом (рис. 51):
SELECT COUNT(*) AS level FROM T AS B1, T AS E1 WHERE
E1.L BETWEEN B1.L AND B1.R AND
E1.R BETWEEN B1.L AND B1.R AND
E1.Node='D';
- 71 -
Рис. 51. Использование BETWEEN для определения уровня в иерархии
Для всех узлов в иерархии вычислим их уровни при помощи следующего
запроса (рис. 52):
SELECT P2.Node, COUNT(*) AS level
FROM T AS P1, T AS P2
WHERE P2.L BETWEEN P1.L AND P1.R
GROUP BY P2.Node;
- 72 -
Рис. 52. Уровни всех узлов иерархии
Количество потомков можно определить как половину разности правого
и левого коэффициентов (R-L)/2 (рис. 53)
SELECT (R-L)/2 AS Child FROM T WHERE Node='D';
- 73 -
Рис. 53. Количество потомков узла D
Для элементов, не имеющих потомков, разность между значениями
левого и правого коэффициентов всегда равна 1. Поэтому все листья можно
найти следующим простым запросом (рис. 54):
SELECT * FROM T WHERE R-L=1;
Рис. 54. Список листьев
Выполнение запроса можно ускорить за счёт использования уникального
индекса по столбцу левых коэффициентов. Создадим индекс:
CREATE UNIQUE INDEX L1 ON T(L ASC);
Для того чтобы воспользоваться преимуществом индекса, запрос следует
написать в виде
SELECT * FROM T WHERE L=(R-1);
- 74 -
Обратите внимание. MS SQL Server может применить индекс только в
том случае, когда атрибуты, на которых этот индекс определён, не
используются в выражениях, поэтому если записать условие в виде (RL) = 1, то индекс использоваться не будет.
При данном способе моделирования иерархии некоторую трудность
может создать задача определения всех непосредственных потомков для
заданного узла. Для того чтобы выбрать всех непосредственных потомков
заданного узла, нужно:
1) определить уровень этого заданного узла и нарастить полученное
значение на единицу – это будет значение уровня в дереве для
непосредственных потомков данного узла:
SELECT COUNT(*)+1 AS Level
FROM T AS B3, T AS E3
WHERE E3.L BETWEEN B3.L AND B3.R AND
E3.Node='D';
Результат выполнения этого предложения SQL в среде SQL Server Management
Studio Express представлен на рис. 55.
- 75 -
Рис. 55. значение уровня в дереве для непосредственных потомков узла D
2) выбрать узлы уровня, вычисленного на предыдущем шаге:
SELECT DISTINCT B1.Node FROM T AS B1, T AS E1 WHERE
(SELECT COUNT(*) FROM T AS B2, T AS E2 WHERE
E2.L BETWEEN B2.L AND B2.R AND E2.Node=B1.Node)=
(SELECT COUNT(*)+1 FROM T AS B3, T AS E3 WHERE
E3.L BETWEEN B3.L AND B3.R AND E3.Node='D');
Результат выполнения этого предложения SQL в среде SQL Server Management
Studio Express представлен на рис. 56.
- 76 -
Рис. 56. Выбираем узлы 4-го уровня
3) Окончательно из полученного на предыдущем шаге списка узлов
выбрать те узлы, которые имеют предком заданный узел:
SELECT B1.Node FROM T AS B1, T AS E1
WHERE (SELECT COUNT(*) FROM T AS B2, T AS E2 WHERE
E2.L BETWEEN B2.L AND B2.R AND E2.Node=B1.Node)=
(SELECT COUNT(*)+1 FROM T AS B3, T AS E3 WHERE
E3.L BETWEEN B3.L AND B3.R AND E3.Node='D') AND
B1.L BETWEEN E1.L AND E1.R AND E1.Node='D';
Результат выполнения этого предложения SQL в среде SQL Server Management
Studio Express представлен на рис. 57.
- 77 -
Рис. 57. Непосредственные потомки узла D
Аналогичным образом определяем непосредственного предка для заданного
узла (в примере – для узла D):
FROM T AS B1, T AS E1 WHERE (SELECT COUNT(*) FROM T AS
B2, T AS E2 WHERE
E2.L BETWEEN B2.L AND B2.R AND E2.Node=B1.Node)=
(SELECT COUNT(*)-1 FROM T AS B3, T AS E3 WHERE
E3.L BETWEEN B3.L AND B3.R AND E3.Node='D') AND
E1.L BETWEEN B1.L AND B1.R AND E1.Node='D';
Результат выполнения этого предложения SQL в среде SQL Server Management
Studio Express представлен на рис. 58.
- 78 -
Рис. 58. Определён непосредственный предок узла D
Обратите внимание, что для первого непосредственного потомка
значение левого коэффициента будет на единицу больше значения левого
коэффициента
родительского
узла.
Для
второго
–
значение
левого
коэффициента будет на единицу больше значения правого коэффициента
первого непосредственного потомка и т.д. Для последнего непосредственного
потомка значение правого коэффициента будет на единицу меньше значения
правого
коэффициента
родительского
узла.
Это
свойство
можно
использовать для выборки непосредственных потомков узла в рекурсивной
процедуре.
Для быстрого определения всех непосредственных предков и/или
потомков для некоторого заданного узла таблица может быть расширена
добавлением
столбца,
значения
которого
служат
указателями
непосредственных предков элементов:
ALTER TABLE T ADD Parent int references T(L);
- 79 -
на
или представляют собой уровень в иерархии:
ALTER TABLE T ADD [Level] int;
Тогда задача получения значений непосредственных предков или потомков
будет решаться аналогично тому, как это делается в предыдущем способе.
Выполните такие выборки самостоятельно.
Теперь рассмотрим задачу поиска ближайшего общего предка для двух
заданных узлов. Пусть это будут узлы J и K. Как видно на рис. 43, их
ближайший общий предок – узел D.
Список всех общих для узлов J и K предков можно получить, например,
следующим SQL-предложением (рис. 59):
select L, [Node], R from T where L<=(
select MIN(L) from T where Node='J' OR Node='K') AND
R>=(select MAX(R) from T where Node='K' OR Node='K');
Рис. 59. Список всех общих предков узлов J и K
Нужное нам значение ближайшего общего узла можно определить как узел из
данного
списка,
имеющий,
например,
- 80 -
максимальное
значение
левого
коэффициента (рис. 60) (или минимальное значение правого коэффициента, или
минимальное значение разности значений правого и левого коэффициентов).
select MAX(L) from T where
L<=(select MIN(L) from T where Node='J' OR Node='K') AND
R>=(select MAX(R) from T where Node='K' OR Node='K');
Рис. 60. Узел с максимальным значением левого коэффициента
Окончательно, находим требуемое значение (рис. 61):
select L, [Node], R from T where L=(
select MAX(L) from T where (
L<=(select MIN(L) from T where Node='J' OR Node='K')
AND R>=(select MAX(R) from T where Node='K' OR
Node='K')));
- 81 -
Рис. 61. Ближайший общий предок узлов J и K
Отдельно рассмотрим задачи удаления и добавления узлов в дерево.
Если требуется удалить из иерархии поддерево, можно выполнить следующее
предложение SQL:
DELETE FROM T WHERE L BETWEEN
(SELECT L FROM T WHERE Node='D')
AND
(SELECT R FROM T WHERE Node='D');
Результат выполнения этого предложения SQL в среде SQL Server Management
Studio Express представлен на рис. 62.
- 82 -
Рис. 62. Удаление узлов
После
выполнения
этого
запроса
появляются
промежутки
в
последовательности правого и левого коэффициентов (рис. 63). Это не мешает
выполнять большинство запросов к дереву, так как свойство вложенности
сохраняется. Можно, например, использовать предикат BETWEEN в запросах,
но другие операции, зависящие от «плотности» номеров коэффициентов, не
будут работать в дереве с промежутками. Например, теперь нельзя выбрать все
листья, используя свойство (R-L)=1, невозможно определить число узлов в
поддереве, используя значения левого и правого коэффициентов его корня.
- 83 -
Также, «благодаря» данному запросу, утрачена информация, которая была бы
очень полезна в закрытии образовавшихся промежутков – правильные и левые
номера корня поддерева. Поэтому такой запрос применять не следует!
Рис. 63. Разрыв в нумерации
Для того чтобы удаление поддерева было корректным, следует восстановить
«плотность» номеров правого и левого коэффициентов (рис. 64). Для этого
лучше использовать хранимую процедуру.
- 84 -
Рис. 64. Восстановленная «плотность» значений правого и левого
коэффициентов
Обратите внимание. Триггер здесь неприменим, так как невозможно
автоматически
определить,
что
именно
надо
сделать
–
удалить
единственный узел или всё поддерево, имеющее корнем заданный узел.
В случае отсутствия в используемой СУБД таких механизмов, следует
выполнить эту операцию непосредственно в приложении.
Приведём пример хранимой процедуры для удаления поддерева:
CREATE PROCEDURE DropTree (@downsized VARCHAR) AS
-- ВНИМАНИЕ! Тут должна быть проверка входного параметра!
-- После каждой операции необходима проверка ошибок,
-- отсутствующая в данном примере!
BEGIN
DECLARE @dropNode VARCHAR;
DECLARE @dropL INT;
DECLARE @dropR INT;
-- Транзакция
BEGIN TRANSACTION DropTreeTransaction;
-- Сохраняем данные поддерева
SELECT @dropNode=Node, @dropL=L, @dropR=R
FROM T WHERE Node=@downsized;
-- Удаляем поддерево
DELETE FROM T WHERE L BETWEEN @dropL and @dropR;
-- Уплотняем промежутки
UPDATE T SET L=CASE
WHEN L>@dropL THEN L-(@dropR-@dropL+1)
ELSE L END,
- 85 -
R=CASE
WHEN R>@dropL
THEN R-(@dropR-@dropL+1)
ELSE R END;
-- ВНИМАНИЕ! Тут должна быть проверка результатов и,
-- если были ошибки, откат транзакции!
COMMIT TRANSACTION DropTreeTransaction;
SELECT * FROM T;
END;
Обратите внимание! Реальная процедура должна включать в себя обработку
ошибок, отсутствующую в данном примере.
Результат создания хранимой процедуры в среде SQL Server Management Studio
Express представлен на рис. 65.
- 86 -
Рис. 65. Создание хранимой процедуры
Теперь используем созданную нами хранимую процедуру для того, чтобы
удалить поддерево с корнем в узле D:
EXEC DropTree 'D';
На рис. 66 представлен результат выполнения этой операции.
- 87 -
Рис. 66. Удаление поддерева
Сравните полученный результат со структурой на рис. 61.
Удаление отдельных узлов, если они не являются листьями, – более
сложная задача, чем удаление полных поддеревьев (рис. 67-68) (листья
рассматриваются как поддеревья, состоящие из единственного узла, и
удаляются аналогично «настоящим» поддеревьям).
- 88 -
Рис. 67. Удаление единственного узла
Рис. 68. Результат удаления узла D
Как и в случае рекурсивной модели, при удалении одиночного узла в
середине дерева будем подключить потомков удаляемого узла к его предку. В
модели вложенных множеств это выполняется автоматически. При простом
удалении узла, так же как и в случае удаления поддерева, будет образовываться
«разрыв» в нумерации правого и левого коэффициентов (см. рис. 67). Поэтому
здесь также требуется хранимая процедура. Приведём пример процедуры,
- 89 -
выполняющей переназначение правого и левого коэффициентов для узлов
иерархии при удалении единственного узла:
CREATE PROCEDURE DropNode (@downsized VARCHAR) AS
-- ВНИМАНИЕ! Тут должна быть проверка входного параметра!
-- После каждой операции необходима проверка ошибок,
-- отсутствующая в данном примере!
BEGIN
DECLARE @dropNode VARCHAR;
DECLARE @dropL INT;
DECLARE @dropR INT;
-- Транзакция
BEGIN TRANSACTION DropNodeTransaction;
-- Сохраняем данные поддерева
SELECT @dropNode=Node, @dropL=L, @dropR=R
FROM T WHERE Node=@downsized;
-- Удаляем узел
DELETE FROM T WHERE Node=@downsized;
-- Уплотняем промежутки
UPDATE T SET L=CASE
WHEN L BETWEEN @dropL AND @dropR THEN L-1
WHEN L>@dropR THEN L-2
ELSE L END,
R=CASE
WHEN R BETWEEN @dropL AND @dropR THEN R-1
WHEN R>@dropR THEN R-2
ELSE R END;
-- ВНИМАНИЕ! Тут должна быть проверка результатов и,
-- если были ошибки, откат транзакции!
COMMIT TRANSACTION DropNodeTransaction;
END;
- 90 -
Результат создания этой хранимой процедуры в среде SQL Server Management
Studio Express представлен на рис. 69.
Рис. 69. Создание хранимой процедуры
Удалим узел D используя эту хранимую процедуру:
EXEC DropNode 'D';
Результат выполнения этой хранимой процедуры в среде SQL Server Management Studio Express представлен на рис. 70.
- 91 -
Рис. 70. Выполнение хранимой процедуры, удаление узла D
Обратите внимание! Наличие хранимых процедур не мешает удалять
элементы иерархии другими способами, например при помощи ранее
рассмотренного предложения SQL DELETE!
- 92 -
Теперь рассмотрим задачу добавления узлов в дерево. Будем помещать
новые узлы справа. Для того чтобы добавить лист в иерархию, создадим
следующую хранимую процедуру:
CREATE PROCEDURE AddNode (@Parent VARCHAR, @New VARCHAR)
AS
-- Добавляем лист справа
-- ВНИМАНИЕ! Тут должна быть проверка входного параметра!
-- После каждой операции необходима проверка ошибок,
-- отсутствующая в данном примере!
BEGIN
DECLARE @ParentR INT;
-- Транзакция
BEGIN TRANSACTION AddNodeTransaction;
-- Получаем правый коэффициент родительского элемента
SELECT @ParentR=R
FROM T WHERE Node=@Parent;
-- Пересчитываем правый и левый коэффициенты
UPDATE T SET L=CASE WHEN L>@ParentR THEN L+2
ELSE L END,
R=CASE
WHEN R>=@ParentR THEN R+2
ELSE R END;
-- Добавляем узел
INSERT INTO T(L, Node, R) VALUES (@ParentR, @New,
@ParentR+1);
-- ВНИМАНИЕ! Тут должна быть проверка результатов и,
-- если были ошибки, откат транзакции!
COMMIT TRANSACTION AddNodeTransaction;
END;
- 93 -
Создание этой хранимой процедуры в среде SQL Server Management Studio Express представлено на рис. 71.
Рис. 71. Создание хранимой процедуры
Добавим к узлу B ещё одного потомка – лист D, как показано на рис. 72.
- 94 -
Рис. 72. Присоединение листа к иерархии
После пересчёта правого и левого коэффициентов иерархия должна иметь вид,
представленный на рис. 73.
Рис. 73. Результат присоединения листа D к иерархии
Вызовем хранимую процедуру для добавления узла:
EXEC AddNode 'B', 'D';
- 95 -
Результат выполнения этой хранимой процедуры в среде MS SQL Server Management Studio Express представлен на рис. 74.
Рис. 74. Результат присоединения листа D к узлу B
Как и в случае удаления элементов иерархии, наличие хранимых
процедур не препятствует добавлению записей в таблицу простым оператором
INSERT. В обоих случаях остаётся опасная возможность для выполнения
действий, могущих повлечь нарушения целостности данных.
- 96 -
Заметим, что пересчёт правого и левого коэффициентов дерева,
выполненный в объемлющем языке, работает быстрее, чем реализованный в
«чистом» SQL.
Таким образом, главный недостаток метода вложенных множеств
состоит в том, что при изменении в структуре дерева (удалении, добавлении
узлов) приходится заново
пересчитывать значения правого
и
левого
коэффициентов для всей таблицы. Это довольно трудоёмкая процедура,
особенно в случае больших иерархий. Поэтому такой способ годится только
для небольших и/или редко изменяемых таблиц.
Обратите внимание. Порядок узлов внутри каждого уровня уже задан
значениями правого/левого коэффициентов. Если требуется поддерживать
несколько различных вариантов упорядочения узлов внутри уровней, можно
добавить дополнительные пары левого и правого коэффициентов для каждого
варианта упорядочивания.
Задачи
Создайте таблицу, описывающую абстрактную иерархию со структурой
с правым и левым коэффициентами. Заполните её данными. На основе этой
таблицы сформируйте и выполните SQL-предложения, решающие следующие
задачи:
1.
Определить корневой элемент иерархии.
2.
Определить непосредственного предка некоторого заданного
элемента.
3.
Определить, что заданный элемент не имеет потомков.
4.
Выбрать все элементы иерархии, не имеющие потомков (листья).
5.
Выбрать всех потомков заданного элемента (выбрать поддерево,
начиная с заданного узла).
6.
Выбрать всех предков для заданного узла.
- 97 -
7.
Определить расстояние от корня до заданного узла.
8.
Определить уровень в иерархии заданного узла относительно
другого узла.
Задачи повышенной сложности
1.
Выбрать всех непосредственных потомков для некоторого
заданного элемента.
2.
Найти ближайшего общего предка для нескольких наперёд
заданных узлов.
3.
Написать хранимые процедуры для пересчёта правого и левого
коэффициентов при удалении узлов в иерархии.
4.
Написать хранимые процедуры для пересчёта правого и левого
коэффициентов при добавлении узлов в иерархию.
5.
Рассмотреть вариант модели, когда значения правого и левого
коэффициентов для листьев в дереве устанавливаются равными.
- 98 -
2.3. Способ вспомогательной таблицы
Способ, описанный Ральфом Кимбаллом, может рассматриваться как
расширенный вариант рекурсивного способа. Здесь модель дерева строится на
основе двух таблиц. Первая таблица (базовая) хранит список всех узлов,
снабжённых
уникальными
идентификаторами,
и
всю
содержательную
информацию по каждому узлу.
Таблица 5
Пример основной таблицы
Уникальный
идентификатор узла
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Содержательная
часть
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Создадим эту таблицу:
CREATE TABLE T_Base
(ID uniqueidentifier DEFAULT NEWID() primary key,
Node varchar not null);
В этом примере для идентификаторов узлов дерева, хранимых в столбце ID,
использован тип данных UNIQUEIDETIFIER, который будет заполняться
автоматически при добавлении записей в таблицу (значение по умолчанию для
- 99 -
этого столбца генерируется функцией NEWID()) и является первичным
ключом.
Заполним таблицу данными (значения для столбца ID вводить не
требуется):
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INSERT
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
INTO
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
T_Base
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
(Node)
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
VALUES
('A');
('B');
('C');
('D');
('E');
('F');
('G');
('H');
('I');
('J');
('K');
('L');
('M');
('N');
('O');
('P');
Выберем все столбцы и строки полученной таблицы, отсортировав строки по
полю Node:
SELECT * FROM T_Base ORDER BY Node;
Результат выполнения этого предложения SQL в среде SQL Server Management
Studio Express представлен на рис. 75.
- 100 -
Рис. 75. Заполнение основной таблицы модели иерархии
Теперь дополним основную таблицу, содержащую перечень всех узлов
иерархии, вспомогательной таблицей. Конечно, иерархию можно было
- 101 -
представить и в виде одной таблицы, но в этом случае содержательная часть
каждого узла будет повторяться для всех строк, связанных с каждым из узлов,
что нежелательно с точки зрения нормализации.
Вспомогательная таблица по своей структуре похожа на таблицу со
ссылкой на непосредственного предка, но, в отличие от неё, содержит все
полные пути от корневого элемента до каждого узла иерархии в виде наборов
пар «родитель-потомок». Здесь потомок может не быть непосредственным
(прямым) потомком родителя, поэтому для каждой такой пары указывается
расстояние («степень родства») от предка до потомка.
Корневые узлы деревьев могут быть обозначены как имеющие в
качестве предка самих себя с нулевым расстоянием или, в вариантном
исполнении, запись, соответствующая корневому узлу дерева, может просто
отсутствовать во вспомогательной таблице.
Первичный ключ во вспомогательной таблице составят все три столбца.
Структура
такой
таблицы
и
ее
содержимое
для
иерархии,
представленной на рис. 1, показана в таблице 6.
Таблица 6
Пример структуры вспомогательной таблицы
Уникальный
идентификатор
узла
Указатель на Расстояние
предка узла
до предка
A A или NULL
B
A
C
A
D
A
D
B
E
A
E
B
F
A
F
C
G
A
G
C
H
A
- 102 -
0
1
1
2
1
2
1
2
1
2
1
2
Уникальный
идентификатор
узла
Указатель на Расстояние
предка узла
до предка
H
I
I
I
J
J
J
K
K
K
L
L
L
O
O
O
M
M
M
N
N
N
P
P
P
C
A
B
D
A
B
E
A
B
E
A
B
E
A
C
F
A
C
G
A
C
G
A
C
H
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
Создадим вспомогательную таблицу для уже имеющейся базовой
таблицы с помощью следующего предложения SQL:
CREATE TABLE T_Helper (
UID uniqueidentifier references T_Base(ID) NOT
NULL,
ParentID uniqueidentifier references T_Base(ID)
NOT NULL,
[Level] int NOT NULL,
PRIMARY KEY (UID, ParentID, [Level]));
После заполнения данными базовая таблица имеет следующий вид:
- 103 -
Таблица 7
Таблица T_Base после заполнения данными
ID
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
BB31DC0F-BB57-48FF-B768-79D205F17F33
07EDFC1D-2506-426E-BE2A-38BC86BA9EA2
777A3A0E-6079-43B9-8B9D-325230D48A3A
CF8D4BA4-89E3-4729-9B12-DDF2987D5949
9A42488B-536B-4269-A90C-BE164269425B
8E8CD289-7098-4C4F-A5A1-3DF2C25963AD
D9826E05-BFD1-4845-AEFC-215C2E5BA78B
AF1A9E1A-4747-4BC1-A5F5-FA30272BD4A8
AEB398A7-F653-4BE8-8844-AE9CAEC1A26F
2520E3D9-B40F-4A6A-ADD8-A334CABCFBB2
0FBD7863-B4B5-456A-8FAC-63B972AAD010
5BDF65BE-2940-48F3-BBB1-B3BA8B48287B
561D8D98-9410-4ECC-BA0E-CE4CC204705B
FC579270-CA86-4153-8DA3-BEA0F5DB6E24
Node
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Обратите внимание! Значения в столбце ID на каждом рабочем месте
будут свои! Они также будут различаться и на одном рабочем месте при
каждой новой попытке заполнения таблицы.
Созданную вспомогательную таблицу заполним данными, используя
уникальные идентификаторы, автоматически назначенные узлам иерархии при
заполнении базовой таблицы.
NSERT INTO T_Helper (UID, ParentID, Level) VALUES (
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',0);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES (
'06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES (
'BB31DC0F-BB57-48FF-B768-79D205F17F33',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES (
'07EDFC1D-2506-426E-BE2A-38BC86BA9EA2',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES (
'07EDFC1D-2506-426E-BE2A-38BC86BA9EA2',
'06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES (
- 104 -
'777A3A0E-6079-43B9-8B9D-325230D48A3A',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'777A3A0E-6079-43B9-8B9D-325230D48A3A',
'06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'CF8D4BA4-89E3-4729-9B12-DDF2987D5949',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'CF8D4BA4-89E3-4729-9B12-DDF2987D5949',
'BB31DC0F-BB57-48FF-B768-79D205F17F33',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'9A42488B-536B-4269-A90C-BE164269425B',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'9A42488B-536B-4269-A90C-BE164269425B',
'BB31DC0F-BB57-48FF-B768-79D205F17F33',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'8E8CD289-7098-4C4F-A5A1-3DF2C25963AD',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'8E8CD289-7098-4C4F-A5A1-3DF2C25963AD',
'BB31DC0F-BB57-48FF-B768-79D205F17F33',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'D9826E05-BFD1-4845-AEFC-215C2E5BA78B',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'D9826E05-BFD1-4845-AEFC-215C2E5BA78B',
'06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'D9826E05-BFD1-4845-AEFC-215C2E5BA78B',
'07EDFC1D-2506-426E-BE2A-38BC86BA9EA2',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'AF1A9E1A-4747-4BC1-A5F5-FA30272BD4A8',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'AF1A9E1A-4747-4BC1-A5F5-FA30272BD4A8',
'06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'AF1A9E1A-4747-4BC1-A5F5-FA30272BD4A8',
'777A3A0E-6079-43B9-8B9D-325230D48A3A',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'AEB398A7-F653-4BE8-8844-AE9CAEC1A26F',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
- 105 -
(
(
(
(
(
(
(
(
(
(
(
(
(
(
(
'AEB398A7-F653-4BE8-8844-AE9CAEC1A26F',
'06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'AEB398A7-F653-4BE8-8844-AE9CAEC1A26F',
'777A3A0E-6079-43B9-8B9D-325230D48A3A',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'2520E3D9-B40F-4A6A-ADD8-A334CABCFBB2',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'2520E3D9-B40F-4A6A-ADD8-A334CABCFBB2',
'06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'2520E3D9-B40F-4A6A-ADD8-A334CABCFBB2',
'777A3A0E-6079-43B9-8B9D-325230D48A3A',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'561D8D98-9410-4ECC-BA0E-CE4CC204705B',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'561D8D98-9410-4ECC-BA0E-CE4CC204705B',
'BB31DC0F-BB57-48FF-B768-79D205F17F33',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'561D8D98-9410-4ECC-BA0E-CE4CC204705B',
'CF8D4BA4-89E3-4729-9B12-DDF2987D5949',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'0FBD7863-B4B5-456A-8FAC-63B972AAD010',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'0FBD7863-B4B5-456A-8FAC-63B972AAD010',
'BB31DC0F-BB57-48FF-B768-79D205F17F33',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'0FBD7863-B4B5-456A-8FAC-63B972AAD010',
'9A42488B-536B-4269-A90C-BE164269425B',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'5BDF65BE-2940-48F3-BBB1-B3BA8B48287B',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'5BDF65BE-2940-48F3-BBB1-B3BA8B48287B',
'BB31DC0F-BB57-48FF-B768-79D205F17F33',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'5BDF65BE-2940-48F3-BBB1-B3BA8B48287B',
'9A42488B-536B-4269-A90C-BE164269425B',1);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
'FC579270-CA86-4153-8DA3-BEA0F5DB6E24',
'7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4',3);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES
- 106 -
(
(
(
(
(
(
(
(
(
(
(
(
(
(
(
'FC579270-CA86-4153-8DA3-BEA0F5DB6E24',
'BB31DC0F-BB57-48FF-B768-79D205F17F33',2);
INSERT INTO T_Helper (UID, ParentID, Level) VALUES (
'FC579270-CA86-4153-8DA3-BEA0F5DB6E24',
'8E8CD289-7098-4C4F-A5A1-3DF2C25963AD',1);
Таблица 8
Вспомогательная таблица T_Helper после заполнения данными
UID
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
BB31DC0F-BB57-48FF-B768-79D205F17F33
07EDFC1D-2506-426E-BE2A-38BC86BA9EA2
07EDFC1D-2506-426E-BE2A-38BC86BA9EA2
777A3A0E-6079-43B9-8B9D-325230D48A3A
777A3A0E-6079-43B9-8B9D-325230D48A3A
CF8D4BA4-89E3-4729-9B12-DDF2987D5949
CF8D4BA4-89E3-4729-9B12-DDF2987D5949
9A42488B-536B-4269-A90C-BE164269425B
9A42488B-536B-4269-A90C-BE164269425B
8E8CD289-7098-4C4F-A5A1-3DF2C25963AD
8E8CD289-7098-4C4F-A5A1-3DF2C25963AD
D9826E05-BFD1-4845-AEFC-215C2E5BA78B
D9826E05-BFD1-4845-AEFC-215C2E5BA78B
D9826E05-BFD1-4845-AEFC-215C2E5BA78B
AF1A9E1A-4747-4BC1-A5F5-FA30272BD4A8
AF1A9E1A-4747-4BC1-A5F5-FA30272BD4A8
AF1A9E1A-4747-4BC1-A5F5-FA30272BD4A8
AEB398A7-F653-4BE8-8844-AE9CAEC1A26F
AEB398A7-F653-4BE8-8844-AE9CAEC1A26F
AEB398A7-F653-4BE8-8844-AE9CAEC1A26F
2520E3D9-B40F-4A6A-ADD8-A334CABCFBB2
2520E3D9-B40F-4A6A-ADD8-A334CABCFBB2
2520E3D9-B40F-4A6A-ADD8-A334CABCFBB2
561D8D98-9410-4ECC-BA0E-CE4CC204705B
561D8D98-9410-4ECC-BA0E-CE4CC204705B
561D8D98-9410-4ECC-BA0E-CE4CC204705B
0FBD7863-B4B5-456A-8FAC-63B972AAD010
0FBD7863-B4B5-456A-8FAC-63B972AAD010
0FBD7863-B4B5-456A-8FAC-63B972AAD010
5BDF65BE-2940-48F3-BBB1-B3BA8B48287B
5BDF65BE-2940-48F3-BBB1-B3BA8B48287B
5BDF65BE-2940-48F3-BBB1-B3BA8B48287B
FC579270-CA86-4153-8DA3-BEA0F5DB6E24
FC579270-CA86-4153-8DA3-BEA0F5DB6E24
FC579270-CA86-4153-8DA3-BEA0F5DB6E24
ParentID
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
BB31DC0F-BB57-48FF-B768-79D205F17F33
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
BB31DC0F-BB57-48FF-B768-79D205F17F33
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
BB31DC0F-BB57-48FF-B768-79D205F17F33
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
07EDFC1D-2506-426E-BE2A-38BC86BA9EA2
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
777A3A0E-6079-43B9-8B9D-325230D48A3A
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
777A3A0E-6079-43B9-8B9D-325230D48A3A
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
06B5F4F9-4B40-4CC2-982F-F258CDAEBB1F
777A3A0E-6079-43B9-8B9D-325230D48A3A
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
BB31DC0F-BB57-48FF-B768-79D205F17F33
CF8D4BA4-89E3-4729-9B12-DDF2987D5949
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
BB31DC0F-BB57-48FF-B768-79D205F17F33
9A42488B-536B-4269-A90C-BE164269425B
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
BB31DC0F-BB57-48FF-B768-79D205F17F33
9A42488B-536B-4269-A90C-BE164269425B
7F3FF030-0E7D-4BFF-B560-B43CA5E12CE4
BB31DC0F-BB57-48FF-B768-79D205F17F33
8E8CD289-7098-4C4F-A5A1-3DF2C25963AD
Level
0
1
1
2
1
2
1
2
1
2
1
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
Обратите внимание. Аналогично способу со структурой со ссылкой на предка
столбец «Указатель на предка узла» содержит список всех узлов дерева,
имеющих потомков.
- 107 -
Данная модель позволяет проще, чем в случае рекурсивного метода, но
несколько сложнее, чем в случае метода правого и левого коэффициентов (из-за
необходимости связывания двух таблиц), выполнять практически все выборки,
специфические для иерархий.
Приведём несколько простых примеров таких выборок.
Выбор всех потомков для заданного узла (в примере это узел B) может
быть осуществлен следующим предложением SQL:
SELECT T_Base.Node
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.UID and
T_Helper.ParentID=(SELECT ID FROM T_Base
WHERE Node='B');
Результат данного запроса представлен на рис. 76.
Рис. 76. Выборка всех потомков узла B
Выбор всех предков для заданного узла (в примере это узел J)
осуществляется следующим образом:
- 108 -
SELECT T_Base.Node, T_Helper.Level
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.ParentID and
T_Helper.UID=(SELECT ID FROM T_Base
WHERE Node='J')
ORDER BY T_Helper.Level DESC;
Результат выполнения этого запроса представлен на рис. 77.
Рис. 77. Выбор всех предков узла J
Обратите внимание на схожесть этих запросов. Сравните эти запросы с
решением данных задач в случае использования рекурсивной модели иерархии.
Выбор узлов, не имеющих потомков, реализуется, например, следующим
образом:
SELECT DISTINCT T_Base.Node FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.UID and T_Helper.UID NOT IN (
SELECT DISTINCT ParentID FROM T_Helper);
Результат выполнения этого запроса представлен на рис. 78.
- 109 -
Рис. 78. Узлы, не имеющие потомков
Обратите внимание, что данный запрос очень похож на запрос,
который мы использовали для решения той же задачи в случае рекурсивной
модели иерархии.
Теперь рассмотрим задачу отыскания ближайшего общего предка для
двух заданных узлов. Пусть выбраны узлы I и K. Как видно на рис. 8, их
ближайшим общим предком является узел B. Получим полный список общих
предков для узлов I и K (рис. 79):
SELECT T_Base.Node, T_Helper.Level, T_Helper.ParentID
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.ParentID and
T_Helper.UID=(SELECT ID FROM T_Base WHERE
Node='K') AND T_Helper.ParentID IN (
SELECT T_Helper.ParentID
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.ParentID and
T_Helper.UID=(
SELECT ID FROM T_Base WHERE Node='I'));
- 110 -
Рис. 79. Полный список общих предков узлов I и K
Из этого списка надо оставить единственную строку, в которой значение в
столбце Level минимально. Окончательно получаем (рис. 80):
SELECT T_Base.Node, T_Helper.Level, T_Helper.ParentID
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.ParentID AND
T_Helper.UID=(SELECT ID FROM T_Base WHERE
Node='K') AND
T_Helper.ParentID IN
(SELECT T_Helper.ParentID
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.ParentID AND
T_Helper.UID=(SELECT ID FROM T_Base WHERE
Node='I')) AND
T_Helper.Level=(SELECT MIN(T_Helper.Level)
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.ParentID AND
T_Helper.UID=(SELECT ID FROM T_Base WHERE
Node='K') AND
T_Helper.ParentID IN
(SELECT T_Helper.ParentID
FROM T_Base, T_Helper
- 111 -
WHERE T_Base.ID=T_Helper.ParentID AND
T_Helper.UID=(SELECT ID FROM T_Base WHERE
Node='I')));
Рис. 80. Ближайший общий предок узлов I и K
Поддержка целостности структуры иерархии требует дополнительныx
средств.
Приведём пример хранимой процедуры для добавления листьев в дерево
(триггер
и
в
этом
случае
неприменим,
так
как
надо
указывать
непосредственного предка добавляемого узла):
CREATE PROCEDURE AddNode (@Parent VARCHAR, @New VARCHAR)
AS
-- Добавляем лист
- 112 -
-----
ВНИМАНИЕ! Тут должна быть проверка
входного параметра!
После каждой операции необходима проверка ошибок,
отсутствующая в данном примере!
BEGIN
DECLARE @ParentID uniqueidentifier;
DECLARE @NewID uniqueidentifier;
-- Транзакция
BEGIN TRANSACTION AddNodeTransaction;
-- Добавляем строку нового узла в базовую таблицу
INSERT INTO T_Base(Node) VALUES (@New);
-- Получаем уникальный идентификатор нового элемента
SELECT @NewID=ID FROM T_Base WHERE Node=@New;
-- Получаем уникальный идентификатор
-- родительского элемента
SELECT @ParentID=ID FROM T_Base WHERE
Node=@Parent;
-- ВНИМАНИЕ! Тут должна быть проверка
-- на существование родительского узла!
-- Добавляем строку с новым листом
-- во вспомогательную таблицу
INSERT INTO T_Helper(UID, ParentID, [Level])
VALUES (@NewID, @ParentID, 1);
-- Добавляем во вспомогательную таблицу строки
-- указывающие предков предка
INSERT INTO T_Helper(UID, ParentID, [Level])
SELECT @NewID, ParentID, [Level]+1
FROM T_Helper
WHERE UID=@ParentID AND
-- условие [Level]>0 нужно для отсечения
-- вставки дублей записей
-- указывающих на корень дерева
-- (иначе получаем сообщение об ошибке)
[Level]>0;
-- ВНИМАНИЕ! Тут должна быть проверка результатов и,
-- если были ошибки, откат транзакции!
- 113 -
COMMIT TRANSACTION AddNodeTransaction;
END;
Создадим эту хранимую процедуру (рис. 81)
Рис. 81. Создание хранимой процедуры
При помощи этой процедуры добавим в нашу иерархию новый узел Z –
потомок узла P:
EXEC AddNode 'P', 'Z';
Проверим правильность добавления узла Z, выбрав всех его предков:
SELECT T_Base.Node, T_Helper.Level
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.ParentID and
T_Helper.UID=(
- 114 -
SELECT ID FROM T_Base WHERE Node='Z')
ORDER BY T_Helper.Level DESC;
В результате должен быть получен следующий набор узлов: A–C–H–P
(рис. 82).
Рис. 82. Проверка правильности добавления узла Z
Эта хранимая процедура, добавляющая узлы, требует наличия уже
существующей записи для корневого узла. Для добавления корневого узла
создадим специальную хранимую процедуру:
CREATE PROCEDURE AddRootNode (@NewRoot VARCHAR) AS
-- Добавляем корневой элемент иерархии
-----
ВНИМАНИЕ! Тут должна быть проверка
входного параметра!
После каждой операции необходима проверка ошибок,
отсутствующая в данном примере!
BEGIN
DECLARE @RootID uniqueidentifier;
- 115 -
-- Транзакция
BEGIN TRANSACTION AddNodeTransaction;
-- Добавляем строку нового узла в базовую таблицу
INSERT INTO T_Base(Node) VALUES (@NewRoot);
-- Получаем уникальный идентификатор нового элемента
SELECT @RootID=ID FROM T_Base WHERE Node=@NewRoot;
-- Добавляем строку корневого элемента во вспомогательную
таблицу
INSERT INTO T_Helper(UID, ParentID, [Level])
VALUES (@RootID, @RootID, 0);
-- ВНИМАНИЕ! Тут должна быть проверка результатов и,
-- если были ошибки, откат транзакции!
COMMIT TRANSACTION AddNodeTransaction;
END;
Теперь первичное заполнение структуры данных можно реализовать
следующей последовательностью вызовов хранимых процедур:
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
EXEC
AddRootNode 'A';
AddNode 'A', 'B';
AddNode 'A', 'C';
AddNode 'B', 'D';
AddNode 'B', 'E';
AddNode 'D', 'I';
AddNode 'E', 'J';
AddNode 'E', 'K';
AddNode 'E', 'L';
AddNode 'C', 'F';
AddNode 'C', 'G';
AddNode 'C', 'H';
AddNode 'F', 'O';
AddNode 'G', 'M';
AddNode 'G', 'N';
AddNode 'H', 'P';
- 116 -
Удаление одиночных узлов и поддеревьев может быть выполнено при
помощи триггера. Триггер назначается таблице T_Base на операцию удаления
записей. Будем полагать, что при удалении узла удаляются и все его потомки.
CREATE TRIGGER DeleteNode
ON T_Base
INSTEAD OF DELETE
AS
-- Добавляем узлы дерева
-- ВНИМАНИЕ! После каждой операции необходима
-- проверка наличия ошибок, отсутствующая
-- в данном примере!
DECLARE @ID uniqueidentifier;
-- Определяем уровень вложенности триггера
-- (используются средства MS SQL Server)
-- Выполняем только для внешнего удаления
-- (операция DELETE запрошена вне триггера)
IF TRIGGER_NESTLEVEL(OBJECT_ID('DeleteNode'))=1
BEGIN
-- Внимание! В реальном триггере нужно учесть,
-- что может быть удалено несколько строк
-- Получаем уникальный идентификатор
-- удаляемого элемента
SELECT @ID=ID FROM deleted;
-- Собираем список удаляемых узлов
-- во временной таблице #TempTBL
-- Удаляем временную таблицу
-- (на всякий случай, вдруг она существует)
DROP TABLE #TempTBL;
SELECT DISTINCT UID INTO #TempTBL FROM T_Helper
WHERE ParentID=@ID
-- Удаляем всех потомков удаляемого узла из
-- вспомогательной таблицы
DELETE FROM T_Helper WHERE UID IN (
- 117 -
SELECT * FROM #TempTBL);
-- Удаляем все записи вспомогательной таблицы для
-- удаляемого узла
DELETE FROM T_Helper WHERE UID=@ID;
-- Удаляем всех потомков из основной таблицы
-- Эта операция приводит к рекурсивному запуску
-- данного триггера
DELETE FROM T_Base WHERE ID IN (
SELECT * FROM #TempTBL);
-----
Удаляем все записи основной таблицы
для удаляемого узла
Эта операция приводит к рекурсивному запуску
данного триггера
DELETE FROM T_Base WHERE ID=@ID;
END
ELSE
-- Случай рекурсивного вызова триггера
-- (триггер запущен операцией DELETE
-- выполненной в триггере)
BEGIN
-- Удаляем всех потомков из основной таблицы
DELETE FROM T_Base WHERE ID IN (
SELECT * FROM #TempTBL);
-- Удаляем все записи основной таблицы
-- для удаляемого узла
DELETE FROM T_Base WHERE ID=@ID;
END
Обратите внимание. При создании триггера используется параметр INSTEAD
OF для того, чтобы триггер полностью заменял собой команду DELETE,
приводящую к его запуску. Это нужно потому, что базовая и вспомогательная
таблицы связаны между собой и удаление записей из базовой таблицы
невозможно, если существуют основанные на них записи во вспомогательной
таблице!
Удалим добавленный в предыдущем примере узел Z (рис. 83):
- 118 -
DELETE FROM T_Base WHERE Node='Z';
Рис. 83. Удаление узла Z
Теперь попробуем удалить узел C и всё поддерево, для которого этот
узел является корнем (рис. 84):
DELETE FROM T_Base WHERE Node='C';
- 119 -
Рис. 84. Удаление поддерева
Проверим результат выполнения операции удаления, выбрав все узлы
дерева, начиная с корня:
SELECT
T_Base.Node, T_Helper.[Level]
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.UID and
T_Helper.ParentID=(
SELECT ID FROM T_Base WHERE Node='A')
ORDER BY T_Helper.[Level] ASC, T_Base.Node;
Результат не должен содержать узлов C F G H O M N P (рис. 85-86).
- 120 -
Рис. 85. Удаление узлов C F G H O M N P
Рис. 86. Иерархия после удаления поддерева с корневым узлом C
- 121 -
Контроль отсутствия петель в дереве может осуществляться проверкой
записей во вспомогательной таблице – в ней не должно быть записей, в
которых узел является сам себе предком с ненулевым расстоянием (рис. 87).
SELECT T_Base.Node, T_Helper.[Level]
FROM T_Base, T_Helper
WHERE T_Base.ID=T_Helper.UID AND
T_Helper.ParentID=T_Helper.UID AND
-- Исключаем корневые узлы
T_Helper.[Level]>0
ORDER BY T_Base.Node;
Рис. 87. Проверка на наличие/отсутствие петель
Тем не менее, возможность образования петель сохраняется, но на
операциях выборки не может образоваться бесконечный цикл.
Данный
способ,
на
сегодняшний
день,
считается
наиболее
универсальным для представления иерархий средствами реляционной СУБД.
Однако он наследует такой недостаток способа со структурой со ссылкой на
предка, как возможность образования петель. Трудности может вызвать
поддержка целостности для полных путей от корня дерева до указанного узла.
- 122 -
Кроме того, этот способ отличается некоторой избыточностью хранимых
данных.
Задачи
Создайте таблицу, описывающую абстрактную иерархию со структурой
со вспомогательной таблицей. Заполните её данными. На основе этой таблицы
сформируйте и выполните SQL-предложения, решающие следующие задачи:
1.
Определить корневой элемент иерархии.
2.
Определить непосредственного предка некоторого заданного
элемента.
3.
Определить, что заданный элемент иерархии не имеет потомков.
4.
Выбрать все элементы иерархии, не имеющие потомков (листья).
5.
Выбрать всех потомков заданного элемента (выбрать поддерево,
начиная с заданного узла).
6.
Выбрать всех предков для заданного узла.
7.
Определить расстояние от корня до заданного узла.
8.
Определить уровень в иерархии заданного узла относительно
другого узла, не являющегося предком данного.
9.
Выбрать всех непосредственных потомков для некоторого
заданного элемента.
10. Найти ближайшего общего предка для заданных узлов.
11. Удалить заданный узел. Учесть возможность существования
потомков у удаляемого узла.
12. Добавить в иерархию новый элемент как потомка заданного узла.
Написать триггер, добавляющий во вспомогательную таблицу строки для
указания предков, не являющихся для добавляемого узла непосредственными.
13. Написать хранимую процедуру, проверяющую структуру на
отсутствие петель.
14. Проверить все узлы иерархии на наличие нескольких предков.
- 123 -
Попробуйте решить предложенные задачи при условии, что в таблицах
хранится несколько несвязанных иерархий.
- 124 -
3.
Два важных частных случая
В общем случае дерево может иметь любое количество уровней, а узел –
любое количество потомков. В практических задачах число уровней и/или
количество потомков может быть ограничено. Для этих случаев существуют
специальные варианты моделирования иерархий средствами реляционных
СУБД, которые мы здесь кратко рассмотрим.
3.1. Случай ограниченного количества уровней иерархии
Пусть иерархия не может иметь более n уровней, а каждый узел может
иметь любое количество потомков. Тогда её можно представить в виде
следующей таблицы:
Таблица 9
Модель иерархии с ограниченным количеством предков
Идентификатор узла
Предок 1
…
Предок n-1
Содержательная
часть
Для нашего примера абстрактной иерархии (рис. 8), состоящей из 4
уровней, если полагать количество уровней в ней фиксированным, заполненная
таблица будет выглядеть следующим образом:
Таблица 10
Заполненная таблица модели иерархии с ограничением по числу предков
Уникальный
идентификатор
узла
Предки
A
B
C
1
2
3
NULL
A
A
NULL
NULL
NULL
NULL
NULL
NULL
- 125 -
Содержательная
часть
A
B
C
D
E
F
G
H
I
J
K
L
O
M
N
P
A
A
A
A
A
A
A
A
A
A
A
A
A
B
B
C
C
C
B
B
B
B
C
C
C
C
NULL
NULL
NULL
NULL
NULL
D
E
E
E
F
G
G
H
D
E
F
G
H
I
J
K
L
O
M
N
P
Как в случае вспомогательной таблицы, в этом способе в таблице
хранятся все полные пути от корня до каждого из узлов иерархии. В отличие от
способа вспомогательной таблицы, для каждого полного пути от корня до узла
используется только одна строка таблицы. Поэтому соответствующие выборки
данных будут выполняться быстрее.
При работе с данной структурой данных некоторую сложность может
составить определение уровня конкретного узла в иерархии. Уровень можно
определить как количество столбцов идентификаторов предков, имеющих в
строке данного узла неопределённое значение.
Трудности возникают при неожиданном увеличении количества уровней
в иерархии. В такой ситуации может потребоваться не только добавление в
таблицу новых столбцов, но и переработка прикладного ПО.
Иногда, в зависимости от сложности иерархии, в столбцах, где хранятся
идентификаторы предков элементов, может быть большое количество
неопределённых значений (говорят, что таблица получается «рыхлой»). В этом
случае можно использовать вариант данного подхода, где для каждого уровня
иерархии создаётся отдельная таблица.
Вот как в этом случае может быть представлена наша абстрактная
иерархия из четырёх уровней (табл. 11 - 14).
- 126 -
Таблица 11
Таблица, описывающая первый уровень (корень дерева)
Уникальный
идентификатор Содержательная часть
узла
A A
Таблица 12
Таблица второго уровня
Уникальный Предки
идентификатор
Содержательная часть
1
узла
B
A
B
C
A
C
Таблица 13
Таблица третьего уровня
Уникальный
идентификатор
узла
D
E
F
G
H
Предки
1
2
Содержательная
часть
A
A
A
A
A
B
B
C
C
C
D
E
F
G
H
Таблица 14
Таблица четвёртого уровня
Уникальный
идентификатор
узла
I
J
K
L
O
M
N
P
Предки
1
2
3
A
A
A
A
A
A
A
A
B
B
B
B
C
C
C
C
D
E
E
E
F
G
G
H
- 127 -
Содержательная часть
I
J
K
L
O
M
N
P
При такой организации данных количество уровней определяется по
количеству таблиц, а добавление нового уровня требует создания новой
таблицы.
Хотя, в общем случае, нет запрета на применение этого способа и в
случае с произвольным количеством уровней иерархии, при добавлении нового
уровня в дереве потребуется добавление нового столбца в таблицу (или
создание дополнительной таблицы). На практике использование этого подхода
нежелательно из-за трудностей, создаваемых при написании приложений,
использующих подобные структуры данных, – каждый раз в приложении надо
будет учитывать возможность появления в таблице новых столбцов или
появления новых таблиц.
3.2. Случай ограниченного числа потомков
В случае, когда каждый узел может иметь только ограниченное число
потомков, а дерево может иметь произвольное количество уровней, можно
воспользоваться следующей структурой (табл. 15 – 16):
Таблица 15
Модель с полным перечислением потомков
Идентификатор узла Потомок 1 … Потомок K Содержательная часть
Для нашего примера абстрактной иерархии приведённой на рис. 1,
положим, что узлы не могут иметь более трёх потомков. Тогда таблица,
описывающая иерархию, будет выглядеть следующим образом:
Таблица 16
Пример заполненной таблицы
Уникальный
идентификатор
узла
A
Потомки
1
2
3
B
C
NULL
- 128 -
Содержательная часть
A
B
C
D
E
F
G
H
I
J
K
L
O
M
N
P
D
F
I
J
O
M
P
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
E
G
NULL
K
NULL
N
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
H
NULL
L
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
B
C
D
E
F
G
H
I
J
K
L
O
M
N
P
Из приведённого примера видно, что аналогично предыдущему случаю
может получиться довольно «рыхлая» таблица.
Как и в предыдущем случае, этот способ можно применить и для
иерархий с произвольным количеством непосредственных потомков. Но при
каждом увеличении количества потомков потребуется добавление нового
столбца в таблицу, что часто оказывается нежелательным из-за трудностей,
создаваемых при написании приложений, использующих подобную структуру
данных. Каждый раз в приложении надо будет учитывать возможность
появления в таблице новых столбцов.
- 129 -
Заключение
Моделирование деревьев средствами реляционных СУБД всегда
представляет собой нетривиальную задачу.
Вернёмся к определению иерархии или дерева. Дерево определяется как
структура, состоящая из корневого узла, который может быть связан с другими
узлами - «потомками» или «дочерними узлами». Каждый узел - «потомок»
может быть связан со своими узлами - «потомками», причём с любым узлом «потомком» должен быть связан ровно один узел - "предок". Таким образом,
узлы в дереве упорядочены.
Обратите внимание, что в определении отсутствует понятие
«уровень» или «номер уровня».
Исходя
из
определения,
требуется
использование
рекурсивной
процедуры (помним, что любая рекурсия может быть приведена к итерации),
чтобы выполнить обход дерева.
Упорядоченный (линейный) список можно рассматривать как дерево с
единственной ветвью. Иерархию можно представить как набор таких линейных
списков. Сетевая структура или структура типа произвольного графа может
быть представлена как набор деревьев и, следовательно, как набор списков, где
каждый список содержит в себе путь от одного «крайнего» узла до другого.
Таким образом, во всех этих структурах может быть выделено общее свойство
– та или иная упорядоченность элементов.
Реляционная
модель
не
поддерживает
рекурсии
или
итерации.
Отношения в этой модели определяются как неупорядоченные множества
кортежей атомарных элементов. СУБД не поддерживает упорядоченность как
для строк, так и для столбцов в таблицах. Упорядоченность ограничивается
возможностями
оператора
SELECT.
Индексы
- 130 -
также
не
могут
быть
использованы для поддержки структур данных с упорядочением. Отсюда
моделирование любых структур данных, имеющих упорядочение, возможно
только при использовании внешнего по отношению к SQL объемлющего языка,
например языка СУБД для написания встроенных процедур и триггеров или
внешнего языка, на котором пишется приложение.
Если нужно обеспечить быстрое обновление данных, то следует выбрать
такую структуру данных, при которой в базе нужно хранить только указатели
на непосредственных предков узлов, но тогда при выборках не обойтись без
использования рекурсии или итераций в хранимых процедурах или в
приложении.
Если требуется делать быстрые выборки при помощи не очень сложных
предложений SELECT, то нужно явно хранить номер уровня (который неявно
уже хранится в структуре данных как ссылка на предка или указатель на
потомка) или для каждого узла хранить полный путь в дереве (методы «правого
и левого коэффициентов» и «вспомогательной таблицы»).
Заметим, что на практике задачи, где бы потребовалось выбрать не «всех
прямых потомков некоторого узла», а, например, «все узлы 3-го уровня»,
встречаются крайне редко.
При такой организации при некоторой произвольной перестройке дерева
необходимо менять всю информацию о положении множества узлов в
иерархии. Необходимы сложные обновления, при которых целостность дерева
нужно обеспечивать использованием хранимых процедур и механизма
транзакций.
Таким образом, получаем или простые обновления за счёт сложной
выборки, или простые выборки за счёт сложной процедуры обновления.
Проблема
моделирования
иерархий
в
реляционных
СУБД
–
концептуальная проблема. Как уже отмечалось, она заключается в том, что
реляционная модель оперирует неупорядоченными множествами кортежей
атомарных элементов, а для построения иерархий требуется набор операций,
выполняющихся над упорядоченным множеством.
- 131 -
Ожидать в будущем введения каких-то расширений в стандарт SQL,
позволяющих «просто» решить данную задачу, видимо, не стоит –
несоответствие теории даром не проходит, хотя разработчики СУБД и
предлагают
свои
«фирменные»
средства,
уменьшающие
трудности
манипулирования иерархическими данными.
Приведём пример работы с иерархическими данными средствами СУБД
ORACLE из следующей публикации: Кайт Т. О наиболее предпочтительных
особенностях и предложении CONNECT BY (On Favorites and CONNECT BY,
by Tom Kyte) // Oracle Magazine, 2005., May-June.
http://www.oracle.com/technology/oramag/oracle/05-may/o35asktom.html.
Развертывание иерархии
Вопрос. У меня есть таблица, содержащая простую иерархию:
SQL> DESC test_table;
Name
-----A
B
Null?
------
Type
-------------NUMBER(38)
NUMBER(38)
Со следующими данными:
SQL> SELECT * FROM test_table;
A
----------1
2
3
4
5
6
7
8
9
10
11
B
----------1
1
1
2
2
4
4
5
5
3
3
- 132 -
11 rows selected.
Мне нужен запрос, который выдаст мне каждый узел и всех его предков
(родителей). То есть, то, что я должен получить, выглядит так:
A
----------...
9
9
9
9
B
---------9
5
2
1
...
Мне нужно именно это, потому что 9 связана с 9, 9 связана с 5 (в исходной
таблице), 9 косвенно связана с 2 (через 5), и т.д. Как я могу добиться этого
в запросе?
Ответ. На первый взгляд это действительно кажется тяжело, но сделать это
довольно легко в сервере Oracle9i Database, а еще легче в сервере Oracle
Database 10g. Мы легко можем получить всю иерархию:
SQL> SELECT a
2
FROM test_table
3
CONNECT BY PRIOR b=a
4 /
Это действительно позволяет получить ваш столбец B в желательном
результате запроса. Теперь мы должны получить корневой узел каждой из
строк в этой иерархии. Используя довольно новую функцию
SYS_CONNECT_BY_PATH (сервер Oracle9i Database и более поздние версии),
мы можем получить каждый корневой узел. Используя следующий запрос, мы
можем увидеть то, что функция SYS_CONNECT_BY_PATH (SCBP)
возвращает:
SQL> SELECT a,
2
SYS_CONNECT_BY_PATH (a,'.') scbp
3
FROM test_table
4
CONNECT BY PRIOR b=a
5
ORDER BY 2
6 /
A
----------
SCBP
--------- 133 -
...
9
5
2
1
.9
.9.5
.9.5.2
.9.5.2.1
...
Как видите, мы начинаем получать то, что хотим: передняя часть каждого
значения SCBP – корень иерархии, а остаток – столбец A. Теперь мы
воспользуемся небольшим "волшебством" получения подстрок (функция
SUBSTR):
SQL> SELECT a,
2
TO_NUMBER(
3
SUBSTR(scbp,1,
4
INSTR(scbp,'.')-1)
5
) b
6
FROM (
7 SELECT a,
8
LTRIM(
9
SYS_CONNECT_BY_PATH(a,'.'),
10
'.') ||'.' scbp
11
FROM test_table
12
CONNECT BY PRIOR b=a
13
)
14
ORDER BY 2
15 /
A
----------...
9
9
9
9
B
---------9
5
2
1
...
И мы получили то, что нужно. Вы будете рады узнать, что в сервере Oracle
Database 10g это сделать еще легче. Для запросов с предложением CONNECT
BY появилась целая группа новых функций, таких, как:

CONNECT_BY_ROOT – возвращает корень иерархии текущей строки
CONNECT BY, эта функция значительно упрощает наш запрос. (Пример
см. ниже.);
- 134 -

CONNECT_BY_ISLEAF – признак, указывающий, что текущая строка
имеет дочерние строки;

CONNECT_BY_ISCYCLE – признак, указывающий, что в вашей
иерархии текущая строка является началом бесконечного цикла.
Например, если A – родитель B, B – родитель C, а C – родитель A, то у
вас будет бесконечный цикл. Вы можете использовать этот признак для
определения, какая строка или строки ваших данных являются началом
бесконечного цикла;

NOCYCLE – позволяет в запросе с предложением CONNECT BY
распознать, что встретился бесконечный цикл и прекратить выполнение
запроса без выдачи ошибки (вместо возврата ошибки зацикливания при
выполнении предложения CONNECT BY).
Для нашего вопроса важна первая новая функция, CONNECT_BY_ROOT.
Следующий запрос работает на нас в сервере Oracle Database 10g:
SQL> SELECT CONNECT_BY_ROOT a cbr,
2
a b
3
FROM test_table
4
CONNECT BY PRIOR b=a
5
ORDER BY 1
6 /
CBR
----------...
9
9
9
9
B
---------9
5
2
1
...
Можно ли применить приведённый в статье код с другими СУБД,
например с MS SQL Server или IBM DB2?
Если проблема моделирования упорядоченных множеств оказывается весьма
острой, следует обратиться к СУБД, поддерживающим соответствующие
структуры данных, например, к СУБД ADABAS или средствам на основе XML.
- 135 -
Библиографический список
1. Celko, J. Деревья в SQL / J. Celko // DBMS Online. 1996. March.
http://www.dbmsmag.com/9603d06.html
2. Celko, J Trees in SQL / J. Celko // Intelligent Enterprise. 2000. № 10.
3. Celko, J. A Look at SQL Trees / J. Celko //
http://ib.demo.ru/DevInfo/DBMSTrees/9603d06.html
http://ib.demo.ru/DevInfo/DBMSTrees/9604d06.html
http://ib.demo.ru/DevInfo/DBMSTrees/9605d06.html
4. Kimball, R. Help for Hierarchies / R. Kimball // DBMS. 1998. September.
5. Виноградов С.А. Моделирование иерархических объектов / С.А. Виноградов
// http://www.citforum.ru/database/articles/tree.shtml
6. Стулов, А. Особенности построения информационных хранилищ / А. Стулов
// Открытые системы. 2003. №04.
http://www.citforum.ru/database/articles/20030520/
7. Голованов, М. Иерархические структуры данных в реляционных БД / М.
Голованов // RSDN Magazine. 2005. № 0. http://rsdn.ru/article/db/Hierarchy.xml
8. Мухачев, Е. Еще раз об иерархии / Е. Мухачев //
http://www.isp.idknet.com/development/interbase/devinfo/treeadd.htm
9. Христофоров, Ю. Построение дерева иерархии с помощью PHP / MySQL /
Ю. Христофоров // 2004. http://www.activex.net.ru/docs/tree.shtml
10.Кузьменко, Д. Древовидные (иерархические) структуры данных в
реляционных базах данных. Часть 1. / Д. Кузьменко // Epsylon Technologies.
http://www.ibase.ru/devinfo/treedb.htm
11. Кузьменко, Д. Древовидные (иерархические) структуры данных в
реляционных базах данных. Часть 2. / Д. Кузьменко // Epsylon Technologies.
http://www.ibase.ru/devinfo/treedb2.htm
- 136 -
12.Ицик, Бен-Ган. Иерархические структуры, не требующие сопровождения /
Бен-Ган Ицик // SQL Magazine OnLine. 2001. № 5.
http://www2.osp.ru:8083/win2000/sql/2001/05/967.htm
13.Кайт Т. О наиболее предпочтительных особенностях и предложении CONNECT BY (On Favorites and CONNECT BY, by Tom Kyte) / Т. Кайт // Oracle
Magazine, 2005., May-June.
http://www.oracle.com/technology/oramag/oracle/05-may/o35asktom.html
- 137 -
Учебное издание
Ермаков Дмитрий Германович
МОДЕЛИРОВАНИЕ ИЕРАРХИЧЕСКИХ ОБЪЕКТОВ
СРЕДСТВАМИ РЕЛЯЦИОННЫХ СУБД
Редактор Л.Ю. Козяйчева
Компьютерная верстка М.А.Медведева
ИД № 06263 от 12.11.2001 г.
Подписано в печать
Формат 60х80 1/16
Бумага типографская
Плоская печать
Уч.-изд. л. 5,0
Тираж 50
Редакционно-издательский отдел УГТУ-УПИ
620002, Екатеринбург, ул. Мира, 19
rio@mail.ustu.ru
Название и адрес типографии
- 138 -
Усл. печ. л.
Заказ
Download