1
• Adjacency List;• Materialized Path;• Nested Sets;• Так что же использовать?;
Иерархические структуры в реляционных базах данных
2
• Метод хранения Materialized Path;• Виды наследования;• Использование;• Управление;
Materialized Path (Материализованный путь)
3
Метод хранения Materialized Path
Схема:
Правила: • Все пути уникальны;• Все части пути от начала до разделителей должны присутствовать в базе;• Элемент пути не может содержать разделитель (точку);
Свойства: • Для поддержки структуры дерева требуется 1 поле (path);• Узлы напрямую не связаны;• Родительский путь частично входит в дочерние;
path1
path1.path3path1.path2 path1.path4
path1.path3.path7path1.path2.path6path1.path2.path5 path1.path4.path9path1.path4.path8
4
В качестве примеров:
Рубрикатор:
• Товары• Мониторы
• Samsung• 15”• 17”• …
• NEC• 15”• 17”• …
• ViewSonic• 15”• 17”• …
• …• Принтеры
• Samsung• …
• HP• …
• …
Сообщение 1• комментарий 1.1
• ответ на комментарий• …
• … • комментарий 1.2
• ответ на комментарий • …
• …
Сообщение 2• комментарий 2.1
• ответ на комментарий• …
• … • комментарий 2.2
• ответ на комментарий • …
• ……
Комментарии:
5
В качестве примеров:
Рубрикатор:
• Товары• Мониторы
• Samsung• 15”• 17”• …
• NEC• 15”• 17”• …
• ViewSonic• 15”• 17”• …
• …• Принтеры
• Samsung• …
• HP• …
• …
Сообщение 1• комментарий 1.1
• ответ на комментарий• …
• … • комментарий 1.2
• ответ на комментарий • …
• …
Сообщение 2• комментарий 2.1
• ответ на комментарий• …
• … • комментарий 2.2
• ответ на комментарий • …
• ……
Комментарии:
6
Виды наследований
Наследование по вхождению:
goodsgoods.monitorsgoods.monitors.nec.15goods.monitors.nec.17goods.monitors.nec.19goods.monitors.samsunggoods.monitors.samsung.15goods.monitors.samsung.17goods.monitors.samsung.19goods.monitors.viewsonic.15goods.monitors.viewsonic.17goods.monitors.viewsonic.19goods.printersgoods.printers.hpgoods.printers.hp.incgoods.printers.hp.lasergoods.printers.samsunggoods.printers.samsung.laser
7
Виды наследований
goodsgoods. monitorsgoods. monitors. necgoods. monitors. nec. 17goods. monitors. nec. 19goods. monitors. samsunggoods. monitors. samsung. 15goods. monitors. samsung. 17goods. monitors. samsung. 19goods. monitors. viewsonicgoods. monitors. viewsonic. 17goods. monitors. viewsonic. 19goods. printersgoods. printers. hpgoods. printers. hp. incgoods. printers. hp. lasergoods. printers. samsunggoods. printers. samsung. laser
Наследование по вхождению:
8
Виды наследований
goodsgoods. monitorsgoods. monitors. necgoods. monitors. nec. 17goods. monitors. nec. 19goods. monitors. samsunggoods. monitors. samsung. 15goods. monitors. samsung. 17goods. monitors. samsung. 19goods. monitors. viewsonicgoods. monitors. viewsonic. 17goods. monitors. viewsonic. 19goods. printersgoods. printers. hpgoods. printers. hp. incgoods. printers. hp. lasergoods. printers. samsunggoods. printers. samsung. laser
…………17……19…………samsung………laser
…………Мониторы 17”……Мониторы 19”…………Товары марки Samsung………Лазерные принтеры
Наследование по вхождению, множественное:
9
PostgeSQL:
Наследование по вхождению
MySQL:
SELECT t.*, i.* FROM mp_tree AS t JOIN items AS i ON i.id = SUBSTRING_INDEX(t.path, '.', -1);
SELECT t.*, i.* FROM mp_tree AS t JOIN items AS i ON i.id = substring(t.path FROM E'([^\.]+)$')::integer;
Привязка концевых ключей:
Привязка по ключам:
SELECT t.*, i.* FROM items AS i JOIN mp_tree AS t ON t.path LIKE i.id || '.%' OR t.path LIKE '%.' || i.id OR t.path LIKE '%.' || i.id || '.%' WHERE i.id = 2;
10
PostgeSQL:
Наследование по вхождению
CREATE OR REPLACE FUNCTION "public"."mp_path_to_array" ( "path" text)RETURNS integer [] AS$body$DECLARE path_array INTEGER[]; item_alias TEXT;BEGIN FOR item_alias IN SELECT regexp_split_to_table(path, E'\\.') LOOP path_array := array_append(path_array, item_alias::integer); END LOOP; RETURN path_array;END;$body$LANGUAGE 'plpgsql';
SELECT i.* FROM items AS i WHERE i.id = ANY (mp_path_to_array('1.2.3.4.5'));
Привязка по ключам:
11
Множественное наследование:
path TEXT
Структуры таблиц
id INTEGER…
tree items
Множество деревьев:
path TEXTtid INTEGER
id INTEGER…
tree treesWHERE
FUNCTIONS
12
Использование Materialized Path
Задачи:
• Родительский узел;• Подчиненные узлы;• Родительская ветка;• Подчиненная ветка;
13
Получение смежных узлов:
Родительский узел:
SELECT * FROM mp_tree WHERE path = SUBSTRING([path], 1, CHAR_LENGTH([path]) - CHAR_LENGTH(SUBSTRING_INDEX([path], '.', -1)) - 1);
MySQL:
SELECT * FROM mp_tree WHERE path = substring([path] FROM E'^(.+[^\.])\.[^\.]+$');
PostgreSQL:
SELECT * FROM mp_tree WHERE path = subpath([path], 0, -1);
PostgreSQL (ltree):
Использование Materialized Path
14
Получение смежных узлов:
Подчиненные узлы:
SELECT * FROM mp_tree WHERE [path] = SUBSTRING(path, 1, CHAR_LENGTH(path) - CHAR_LENGTH(SUBSTRING_INDEX(path, '.', -1)) - 1);
MySQL:
SELECT * FROM mp_tree WHERE [path] = substring(path FROM E'^(.+[^\.])\.[^\.]+$');
PostgreSQL:
SELECT * FROM mp_tree WHERE path ~ '[path].*{1}';
PostgreSQL (ltree):
Использование Materialized Path
15
Получение родительской ветви:
Общий:
SELECT t.* FROM mp_tree AS t WHERE [path] LIKE t.path || '.%‘ ORDER BY t.path;
Использование Materialized Path
PostgreSQL (ltree):
SELECT t.* FROM mp_tree AS t WHERE t.path <@ [path] AND t.path <> [path] ORDER BY t.path;
16
Получение дочерней ветви:
Общий:
SELECT t.* FROM mp_tree AS t WHERE t.path LIKE '[path].%‘ ORDER BY t.path;
Использование Materialized Path
PostgreSQL (ltree):
SELECT t.* FROM mp_tree AS t WHERE t.path ~ '[path].*' ORDER BY t.path;
17
Управление Materialized PathОсновные действия:
Дополнительные действия:
• Контроль правил;• Денормализация (счетчики и уровень).
• Изменение path узла;• Изменение path подчиненных узлов;
18
Контроль подчиненности:
Триггер PostgreSQL:
CREATE OR REPLACE FUNCTION "public"."mp_tree_update_trigger" () RETURNS trigger AS$body$DECLARE tmp_path TEXT;BEGIN-- Если произошло изменение родителя узла IF NEW.path <> OLD.path THEN-- Проверяем, что бы не поставить родителем потомка IF NEW.path LIKE OLD.path || ‘.%’ THEN RAISE NOTICE 'Нельзя ставить потомком родителя!'; NEW.path := OLD.path; END IF;-- Проверяем, что родитель есть IF NEW.path LIKE '%.%‘ THEN SELECT t.path INTO tmp_path FROM mp_tree AS t WHERE t.path = substring(NEW.path FROM E'^(.+[^\.])\.[^\.]+$'); IF tmp_path IS NULL THEN RAISE NOTICE 'Нельзя ставить несуществующего родителя!'; NEW.path := OLD.path; END IF; END IF; END IF; RETURN NEW;END;$body$LANGUAGE 'plpgsql';
CREATE TRIGGER "mp_tree_update" BEFORE UPDATE ON "public"."mp_tree" FOR EACH ROW EXECUTE PROCEDURE "public"."mp_tree_update_trigger"();
Управление Materialized Path
19
Контроль подчиненности:
Триггер PostgreSQL:
CREATE OR REPLACE FUNCTION "public"."mp_tree_update_trigger" () RETURNS trigger AS$body$DECLARE tmp_path TEXT;BEGIN-- Если произошло изменение родителя узла IF NEW.path <> OLD.path THEN-- Проверяем, что бы не поставить родителем потомка IF NEW.path LIKE OLD.path || ‘.%’ THEN RAISE NOTICE 'Нельзя ставить потомком родителя!'; NEW.path := OLD.path; END IF;-- Проверяем, что родитель есть IF NEW.path LIKE '%.%‘ THEN SELECT t.path INTO tmp_path FROM mp_tree AS t WHERE t.path = substring(NEW.path FROM E'^(.+[^\.])\.[^\.]+$'); IF tmp_path IS NULL THEN RAISE NOTICE 'Нельзя ставить несуществующего родителя!'; NEW.path := OLD.path; END IF; END IF; END IF; RETURN NEW;END;$body$LANGUAGE 'plpgsql';
CREATE TRIGGER "mp_tree_update" BEFORE UPDATE ON "public"."mp_tree" FOR EACH ROW EXECUTE PROCEDURE "public"."mp_tree_update_trigger"();
Управление Materialized Path
20
Управление Materialized PathДенормализация:
Триггер INSERT PostgreSQL (ltree):
CREATE OR REPLACE FUNCTION "public"."_mp_tree_insert_before_trigger" ( ) RETURNS trigger AS $body$ DECLARE new_path ltree; BEGIN IF NEW.path IS NOT NULL THEN --- передан, какой-то материализованный путь ------- обрезаем в материализованном пути id вставляемого узла, если он есть new_path := CASE WHEN NEW.id::text = subpath(NEW.path, -1, 1)::text THEN subpath(NEW.path, 0, -1) ELSE NEW.path END; ------- проверяем существование родителя SELECT mp.path INTO new_path FROM mp_tree AS mp WHERE mp.path = new_path OR mp.id::text = new_path::text; ------- родителя не нашли IF new_path IS NULL OR new_path = '' THEN NEW.path := NEW.id::text; ELSE -- родителя нашли NEW.path := new_path || NEW.id::text; END IF; ELSE NEW.path := NEW.id::text; END IF; RETURN NEW; END; $body$ LANGUAGE 'plpgsql';
CREATE TRIGGER “mp_tree_insert_before" BEFORE INSERT ON "public"."mp_tree" FOR EACH ROW EXECUTE PROCEDURE "public"."_mp_tree_insert_before_trigger"();
21
Управление Materialized PathДенормализация:
Триггер UPDATE PostgreSQL (ltree):
CREATE OR REPLACE FUNCTION "public"."_mp_tree_update_after_trigger" () RETURNS trigger AS$body$DECLARE tid INTEGER; BEGIN--- Принудительно запрещаем изменять ID NEW.id := OLD.id;--- Приводим NULL значение материализованного пути к ID NEW.path := CASE WHEN NEW.path IS NULL THEN NEW.id::text ELSE NEW.path::text END;--- Есть ли у нас изменения материализованного пути IF NEW.path <> OLD.path THEN------- Проверяем что новое значение материализованного пути------- лежит не пределах подчинения и что на его конце ID IF NEW.path ~ ('*.' || NEW.id::text || '.*{1,}')::lquery OR NEW.path ~ ('*.!' || NEW.id::text)::lquery THEN RAISE EXCEPTION 'Bad path!'; END IF;------- Если уровень больше 1 то стоит проверить родителя IF nlevel(NEW.path) > 1 THEN SELECT m.id INTO tid FROM mp_tree AS m WHERE m.path = subpath(NEW.path, 0, nlevel(NEW.path) - 1); IF tid IS NULL THEN RAISE EXCEPTION 'Bad parent!'; END IF; END IF;------- Обновляем детей узла следующего уровня UPDATE mp_tree SET path = NEW.path || id::text WHERE path ~ (OLD.path::text || '.*{1}')::lquery; END IF; RETURN NEW;END;$body$LANGUAGE 'plpgsql'; CREATE TRIGGER “mp_tree_update_after" AFTER UPDATE ON "public"."mp_tree" FOR EACH ROW EXECUTE PROCEDURE "public"."_mp_tree_update_after_trigger"();
22
Управление Materialized PathДенормализация:
Триггер DELETE PostgreSQL (ltree):
CREATE OR REPLACE FUNCTION "public"."_mp_tree_delete_after_trigger" () RETURNS trigger AS $body$BEGIN DELETE FROM mp_tree WHERE path ~ (OLD.path::text || '.*{1}')::lquery; RETURN OLD;END;$body$LANGUAGE 'plpgsql';
CREATE TRIGGER "mp_tree_delete_after" AFTER DELETE ON "public"."mp_tree" FOR EACH ROW EXECUTE PROCEDURE "public"."_mp_tree_delete_after_trigger"();
23
Вопросы?
Карикатуры: Сергей Корсун
Статьи по теме: http://doc.prototypes.ru/database/trees/
Сергей ТомулевичRambler, Москва, 2010