По материалам статьи Eugene Lepekhin: Trees in SQL databases
Статья рассказывает о том, как улучшить производительность деревьев в SQL.
Рано или поздно в своём проекте баз данных Вы столкнетесь с необходимостью хранения иерархической информации. Например: структура предприятия, категории товаров, каталог изделий или папок с документами; всё это хорошие примеры иерархических структур. Конечно же, можно реализовать их хранение в виде самосвязанной таблицы. Проблемы начнутся, когда нужно будет создавать запросы к иерархическим данным. Например: "Что, если мы имеем дело со структурой предприятия, и хотим узнать, сколько служащих подчиняются менеджеру X?". Такие решения подобных проблем уже были описаны. Например, можно посмотреть книгу и статьи Joe Celko, в которых есть подробные описания таких решений.
Упомянутые выше методы хороши, когда необходимо только читать данные, но когда потребуется изменять дерево, может наблюдаться значительное снижение производительности.
Автор собирается предложить свой метод, сравнимый по производительности чтения, но более производительный при модификации дерева.
Предположим, что мы имеем следующую таблицу:
Где: NodeId - первичный ключ, ParentId - внешний ключ и NodeName - некоторое дополнительное поле. Чтобы упростить последующие примеры, давайте предположим, что NodeName имеет ограничение уникальности.
Для всех представленных ниже примеров автор будет использовать следующее дерево:
Создадим ещё одну таблицу, имеющую две связи с таблицей Node:
В этой таблице NodeId и ParentId - внешние ключи, ссылающиеся на таблицу Node.
Каждая запись в таблице Tree означает, что указанный в поле Tree.NodeId узел имеет предка, указанного в поле Tree.ParentId. Под предком автор понимает любой другой узел, расположенный на пути от узла к корню дерева, то есть прямого родителя или главного родителя или самого главного родителя и так далее.
Теперь давайте убедимся, что справедлив один простой инвариант: все предки каждого узла таблицы Node перечислены в таблице Tree.
Например, для узла 4 они могут быть 2 и 1.
Непосредственный вывод из этого инварианта: очень просто можно получить полный путь от узла до корня дерева. Для этого нужно получить всё записи из таблицы Tree, связанные с искомым узлом. Давайте составим соответствующий запрос. Предположим, что мы должны найти путь к корню от узла 7.
Запрос 1:
|
Получился простой и понятный запрос, не правда ли?
Второй вывод из упомянутого выше инварианта: мы можем получить всех потомков узла. Из-за этого в таблице Tree перечисляются все предки для каждого узла в таблицы Node, включая конечно же всех предков искомого узла. Чтобы получить полное поддерево узла, достаточно запросить все узлы, которые имеют искомый узел в качестве предка.
Запрос 2:
|
Этот запрос не менее простой, как и предыдущий. Обратите внимание, что эти два запроса выдают результат в обратном порядке записей таблицы Tree.
Перед тем, как двигаться дальше, автор хотел бы сделать два примечания.
- № 1: Из своего опыта, автор считает, что очень удобно иметь еще одно поле в таблице Tree, которое бы хранило величину удаления в дереве между узлами, указанными в NodeId и ParentId, другими словами - уровень предка. Поэтому, предлагаемая фактическая структура включает поле Level:
№ 2: Самыми распространёнными задачами, являются задачи, использующие поддеревья узла или путь к узлу от корня, включая сам этот узел. Так что было бы полезно иметь в таблице Tree непосредственно и сам узел в качестве собственного предка с уровнем 0.
Можно реализовать описанный инвариант в виде хранимой процедуры или в виде триггеров. Автор предпочитает делать это в одном триггере.
Давайте начнём с триггера на вставку (INSERT) для таблицы Node. Для примеров автор будет использовать MS SQL Server.
Прежде всего, триггер должен создать запись, удовлетворяющую примечанию № 2. Это можно сделать следующей инструкцией:
|
После этого можно добавить список всех предков вставленного узла. Имея ввиду, что родитель каждого вставляемого узла уже имеет список всех своих предков, поэтому мы можем использовать их за основу. Но мы должны заменить NodeId родителя на id нового узла и увеличить уровень.
|
Таким образом, код триггера будет иметь следующий вид:
|
Теперь пришло время продумать триггер на UPDATE, который позаботится о родителе изменяемого узла. Цель триггера на UPDATE состоит в том, чтобы удалить все устаревшие записи в таблице Tree и заменить их новыми. Чтобы минимизировать изменения, совершаемые этим триггером, давайте повторно использовать ту информацию, которая уже имеется в таблице Tree, аналогично тому способу, который мы использовали в триггере на вставку записи. Но разве нельзя просто изменить родителя узла?
Очевидно, что непосредственно для изменяемого узла только одна запись удовлетворяет примечанию № 2. У потомков будут правильными те записи о предках, которые расположены ниже изменяемого узла, включая и сам этот узел. В то же время, каждый находящийся выше изменяемого узла предок будет устаревшим.
Например, если мы изменяем родителя для узла 4, тогда для узлов 5, 6 и 7 будут изменены только те предки, которые выше узла 4, то есть 1 и 2.
На первом шаге мы копируем всех потомков изменяемых узлов вот в такую таблицу:
|
Туда мы вставляем первичные ключи всех потомков и уровни каждого из них относительно изменяемых узлов. Условие, что t.Level > 0, позволяет исключать изменяемый узел из вставки в таблицу @child.
|
Второй шаг удаляет все устаревшие строки у всех потомков:
|
Условие, что t.Level > 0, исключает удаление изменяемых узлов.
Третий шаг удаляет устаревшие записи изменённых узлов:
|
Следующие два шага заполняют таблицу Tree новой информацией. Соответственно, это будет так:
|
Всё это делается так же, как мы это делали в триггере на вставку.
И, наконец, пятый шаг:
|
Может показаться странным, что тут не используются какие-либо соединения с таблицей @child. Но эта инструкция - именно то, что нам здесь нужно, потому что мы собираемся повторить информацию о предке для каждого детишки. Также обратите внимание на то, как мы добавляем уровень детишек, хранящийся в таблице @child, который скорее всего только 1.
Весь триггер будет таким:
|
Как Вы можете видеть, это будет работать только если изменялся ParentId, и все другие изменения таблицы Node будут отфильтрованы самым первым оператором в условии.
Если Вы собираетесь одновременно вставлять или изменять более одного узла, убедитесь, что Вы не делаете это более чем для одного уровня дерева. С практической точки зрения это - не сильное ограничение.
Для завершения функциональности, нужно иметь возможность полностью очищать таблицу Tree и заполнять её заново. Хорошая идея, написать для этого хранимую процедуру, но автор не собирается портить Вам удовольствие создать её самостоятельно.
Комментариев нет:
Отправить комментарий