50. Виды наследований1. Одиночное наследование:idpidid INTEGERidpididpidpididpid INTEGER…idpididpididpididpididpid1. Множественное наследование:idpididpididpididpididpidid INTEGER?idpidsidpidspidsidpidsidpidsid?pid INTEGER[]…idpidsidpidsidpidsidpidsidpidsИерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
89. Подчиненная ветка;Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
94. Использование Adjacency ListПолучение смежных узлов:Родительский узел:SELECT * FROMmy_treeWHERE id = [pid];Подчиненные узлы:SELECT * FROMmy_treeWHEREpid = [id];Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
95. Использование Adjacency ListПолучение родительской ветви:С использованием JOIN:SELECT * FROMmy_treeAS l4 JOINmy_treeAS l3 ON l3.id = l4.pid JOINmy_treeAS l2 ON l2.id = l3.pid JOINmy_treeAS l1 ON l1.id = l2.pid ... WHERE l4.id = [item.pid];С использованием UNION:SELECT l1.*, 1 AS levelFROMmy_treeAS l1 WHERE l1.id = [item.pid]UNIONSELECT l2.*, 2 AS level FROMmy_treeAS l1 JOINmy_treeAS l2 ON l2.id = l1.pid WHERE l1.id = [item.pid]UNIONSELECT l3.*, 3 AS level FROMmy_treeAS l1 JOINmy_treeAS l2 ON l2.id = l1.pid JOINmy_treeAS l3 ON l3.id = l2.pid WHERE l1.id = [item.pid]...ORDERBY level DESC;Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
96. Использование Adjacency ListПолучение родительской ветви:С использованием WITH RECURSIVE (PostgreSQL 8.4):WITH RECURSIVEparentsAS (SELECT *, ARRAY[t.id] AS exist, FALSE AS cycle FROMmy_treeAS t WHERE id = [item.pid]UNION ALL SELECT t.*, exist || t.id, t.id = ANY(exist) FROMparentsAS p, my_treeAS t WHERE t.id = p.pid AND NOT cycle ) SELECT * FROMparents WHERE NOT cycle LIMIT[max_deep];WITH RECURSIVE мы создаем рекурсивный подзапрос parents.Первый запрос рекурсии, в нем указываем точку отсчета. Второй запрос, собственно, рекурсивный запрос. Поле exist - массив уже полученных id, который мы проверяем в поле cycle, что бы не уйти в бесконечную рекурсию. LIMIT ограничивает глубину выборки ветки, так как родитель у узла может быть только лишь один.Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
97. Использование Adjacency ListПолучение родительской ветви (на уровне приложения):Рекурсивная функция:...subparent_recurse {my%params = @_;# Текущая глубина рекурсии$params{deep} ||= 1;# Хеш ранее выбранных узлов$params{exist} ||= {};# Возвращаем пустой список если превысили максимальную заданную глубину рекурсииreturn () if$params{deep} && $params{max_deep} && $params{deep} > $params{max_deep};# делаем запрос к базе данных на поллучение следующего узлаmy$query = 'SELECT * FROM my_tree WHERE id = ' . $params{pid};my$sth = $dbh->prepare($query); $sth->execute;my$item = $sth->fetchrow_hashref;$sth->finish;# Объявляем внутренний список узлов – родителейmy@parents= ();# Если узел найден и у него явно есть родитель, тоif($item&& $item->{pid}) {# Для начала проверим, а не выбирали ли мы уже такой родительский узелreturn () if$params{exist}->{$item->{pid}};# Не выбирали, тогда добавляем в хеш$params{exist}->{$item->{id}} = 1;# И вызываем снова рекурсивную функцию@parents = &parent_recurse(pid => $item->{pid},max_deep => $params{max_deep},deep => $params{deep} + 1,exist => $params{exist} ); }# Добавлем выбранный узел к массиву, если естьpush@parents, $itemif$item;# Возвращаем список выбранных узловreturn@parents;}...my@parents = &parent_recurse(pid => $item->{pid}, max_deep => 2);...Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
98. Использование Adjacency ListПолучение родительской ветви (на уровне приложения):Динамический массив:...# Объявляем пустой список родителейmy@parents= ();# Объявляем хеш уже полученных узлов, и добавляем в него id текущего узлаmy%exist= ($item->{id} => 1);# Объявляем динамический массив и вносим в него текущий узелmy@while = ($item);# Максимальная глубина выборкиmy$max_deep = 3;# Текущая глубина выборкиmy$deep = 1;# В цикле отрезаем первый эмемент динамического массива до тех пор пока это возможноwhile (my$item = shift @while && $deep <= $max_deep) {# Прерываем цикл, если узла явно нет родителя или такого родителя мы уже получалиlastif!$item->{pid} || $exist{$item->{pid}};# Делаем запрос к базе данныхmy$query = 'SELECT * FROM my_tree WHERE id = ' . $item->{pid};my$sth = $dbh->prepare($query); $sth->execute;my$parent = $sth->fetchrow_hashref;$sth->finish;# Прерываем цикл, если узел не получилиlastunless$parent;# Добавляем узел в массив родителейpush@parents, $parent;# Добавляем id узла в хеш полученных узлов$exist{$parent->{id}} = 1;# Добавляем узел в динамический массивpush@while, $parent;# Инкрементим текущую глубину$deep++;}...Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
99. Использование Adjacency ListПолучение родительской ветви:Хранимая процедура PostgreSQL:CREATE OR REPLACE FUNCTION "public"."get_parents" (pid_in INTEGER,deep INTEGER) RETURNS SETOF "public"."my_tree" AS$body$DECLAREexist_ids INTEGER[] := ARRAY[0]; -- Для пустого массива плохо работает ALLpid_now INTEGER := pid_in; -- Текущий piddeep_now INTEGER := deep; -- Текущаа глубинаitemmy_tree;BEGINWHILEpid_nowIS NOT NULL AND pid_now > 0 ANDpid_now <> ALL (exist_ids) AND(deep_nowIS NULL OR deep_now > 0)LOOPSELECT * INTOitemFROMmy_treeWHEREid = pid_now;IFitem.idIS NULL THENEXIT;END IF;RETURN NEXT item;pid_now := item.pid;exist_ids := exist_ids || item.id;IFdeep_nowIS NOT NULL THEN deep_now := deep_now – 1;END IF;END LOOP;RETURN;END;$body$LANGUAGE 'plpgsql';-- Для красоты добавим функцию с одним передаваемым параметромCREATE OR REPLACE FUNCTION "public"."get_parents" (pid_in INTEGER) RETURNSSETOF "public"."my_tree" AS$body$SELECT * FROMget_parents( $1, NULL );$body$LANGUAGE'sql';Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
100. Использование Adjacency ListПолучение родительской ветви:Хранимая процедура MySQL:CREATE DEFINER = CURRENT_USER PROCEDURE `get_parents`( IN pid_in INTEGER, IN max_deep INTEGER )BEGINDECLAREid_now INTEGER;DECLAREpid_now INTEGER DEFAULT pid_in;DECLARE field1_now TEXT;DECLAREexist TEXT DEFAULT '|';WHILEpid_now > 0 ANDexistNOT LIKE CONCAT('%|', pid_now, '|%') AND (max_deepIS NULL OR max_deep > 0)DOSELECT * INTOid_now, pid_now, field1_nowFROMmy_treeWHEREid = pid_now;SELECTid_now, pid_now, field1_now;IF (max_deepIS NOT NULL)THENSETmax_deep = max_deep - 1;END IF;SETexist = CONCAT(exist, id_now, '|'); END WHILE;END;Примечание: Возвращается несколько result set.Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
101. Использование Adjacency ListПолучение дочерней ветви:С использованием WITH RECURSIVE (PostgreSQL 8.4):WITH RECURSIVE childrenAS (SELECT *,pid || '|' || [order_field]ASord, ARRAY[id] ASexist, FALSE AScycleFROMmy_treeWHEREpid = [item.id]UNION ALLSELECTt.*,ord || '.' || t.pid || '|' || t.[order_field],exist || t.id,t.id = ANY(exist)FROMchildren AS c,my_tree AS tWHEREt.pid = c.idANDNOTcycleANDarray_length(exist, 1) < [max_deep]) SELECT * FROMchildrenWHERE NOT cycleORDER BY ordLIMIT[limit];Где: [order_field] - поле сортировки в пределах подчинения;
102. [item.id] - ID узла от которого производится выборка;
104. [limit] - количество выбираемых строк;Основной особенностью этого запроса является дополнительное поле path, которое практически является Materializedpath, за исключением того, что мы добавляем в него дополнительное поле сортировки текущего узла.Причем защита от зацикливания (поля exist и cycle) локализована в пределах отдельных ветвей, поэтому это хоть и предохранит от бесконечного зацикливания, но позволит повторно выбрать строки, так что будте внимательны.Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
105. Использование Adjacency ListПолучение дочерней ветви (на уровне приложения):Рекурсивная функция:...subchild_recurse {my%params= @_;# Текущая глубина рекурсии$params{deep} ||= 1;# Хеш ранее выбранных узлов$params{exist} ||= {};# Возвращаем пустой список если превысили максимальную заданную глубину рекурсииreturn () if$params{deep} && $params{max_deep} && $params{deep} > $params{max_deep};# Объявляем внутренний список узлов – потомковmy@children = ();# делаем запрос к базе данных на получение списка подчиненных узловmy$query = 'SELECT * FROM my_tree WHERE pid = ' . $params{pid} . ($params{order} ? ' ORDER BY ' . $params{order} : '');my$sth = $dbh->prepare($query); $sth->execute;while (my$item = $sth->fetchrow_hashref) {# Для начала проверим, а не выбирали ли мы уже такой узелnext () if$params{exist}->{$item->{id}};# Не выбирали, тогда добавляем в хеш$params{exist}->{$item->{id}} = 1;# Добавлем выбранный узел к массивуpush@children, $item;# Вызываем снова рекурсивную функцию для получения подчиненных узлов текущего узлаmy@children_deep = &child_recurse(pid => $item->{id}, max_deep => $params{max_deep}, order => $params{order},deep => $params{deep} + 1, exist => $params{exist},);# Добавляем список подчиненных узлов текущего узлаpush@children, @children_deep; }$sth->finish;# Возвращаем список выбранных узлов return@children;}my@children = &child_recurse(pid => $item->{id}, max_deep => 5, order => 'id'); ...Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
106. Использование Adjacency ListПолучение дочерней ветви (на уровне приложения):Оптимизация выборки:Ввести денормализацию таблицы создав дополнительное поле счетчика потомков. В этом случае можно будет проверять наличие потомков и делать или нет запрос на их получение. Это решение рассмотрено более детально ниже;Производить выборки не построчно, а списочно, для каждого уровня вложенности. Это решение подробно рассмотрим здесь;SELECTchildren.*FROMmy_treeASchildrenJOINmy_treeASparentsONparents.id = children.pidWHEREchildren.pid = ANY([parents_array])ORDER BYparents.[order_field], children.[order_field];Где:[parents_array] - массив id родительских узлов;[order_field] - поле сортировки;Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
107. Использование Adjacency ListПолучение дочерней ветви (на уровне приложения):Динамический массив:...my$max_deep= 5;my$order= 'field1‘;my$limit= 20; my@children= (); my@while= ($item); my%exist= (); my%indexes= (); my$skew= 0; my$pids= []; my$deep_now= 1; while (my$child= shift@while) {nextif$exist{$child->{id}};$exist{$child->{id}} = 1;push@$pids, $child->{id};unless (defined$indexes{$child->{pid} || 'NULL'}) {if ($childne$item) {push@children, $child;$indexes{$child->{id}} = $#children; } } else {splice@children, $indexes{$child->{pid}} + $skew+ 1, 0 , ($child); $indexes{$child->{id}} = $indexes{$child->{pid}} + $skew+ 1;$skew++; }unless ($while[0]) {$skew= 0;$deep_now++;lastif$max_deep&& $deep_now> $max_deep;my$query= 'SELECT children.* FROM my_tree AS childrenJOIN my_tree AS parents ON parents.id = children.pidWHERE children.pid = ANY(?) ORDER BY parents.’.$order.', children.’.$order.($limit ? ' LIMIT ‘.$limit : '');my$sth= $dbh->prepare($query); $sth->execute($pids);while (my$child= $sth->fetchrow_hashref) {push@while, $child}$sth->finish; }}...Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
109. Денормализация (счетчики и уровень).Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
110. Управление Adjacency ListКонтроль подчиненности:Триггер PostgreSQL 8.4 и выше:CREATE OR REPLACE FUNCTION "public"."my_tree_update_trigger" ()RETURNStriggerAS$body$DECLAREtmp_id INTEGER;BEGIN-- Если произошло изменение родителя узлаIFNEW.pidIS NOT NULL AND (NEW.pid <> OLD.pidOROLD.pidIS NULL) THEN-- Пытаемся найти в родителькой ветки нового родителя текущий узелWITH RECURSIVE parentsAS (SELECTt.id, t.pid, ARRAY[t.id] AS exist, FALSE AS cycleFROMmy_treeAStWHEREid = NEW.pidUNION ALL SELECT t.id, t.pid, exist || t.id, t.id = ANY(exist)FROMparentsASp, my_treeAStWHEREt.id = p.pidAND NOT p.cycle )SELECTidINTOtmp_idFROMparentsWHEREid = NEW.idAND NOT cycleLIMIT 1;-- Узел найден, следовательно родителя назначить не можемIFtmp_idIS NOT NULL THENRAISE NOTICE 'Нельзя ставить потомком родителя!';NEW.pid := OLD.pid;END IF;END IF;RETURN NEW;END;$body$LANGUAGE 'plpgsql';CREATE TRIGGER "my_tree_update" BEFORE UPDATEON "public"."my_tree" FOR EACH ROWEXECUTE PROCEDURE "public"."my_tree_update_trigger"();Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
111. Управление Adjacency ListКонтроль подчиненности:Триггер PostgreSQL ниже 8.4:CREATE OR REPLACE FUNCTION "public"."my_tree_update_trigger" ()RETURNStriggerAS$body$DECLAREtmp_id INTEGER;BEGIN-- Если произошло изменение родителя узлаIFNEW.pidIS NOT NULL AND (NEW.pid <> OLD.pid OR OLD.pidIS NULL) THEN-- Пытаемся найти в родителькой ветки нового родителя текущий узелSELECT idINTOtmp_idFROMget_parents(NEW.pid) WHEREid = NEW.id LIMIT 1;-- Узел найден, следовательно родителя назначить не можемIFtmp_idIS NOT NULL THENRAISE NOTICE 'Нельзя ставить потомком родителя!';NEW.pid := OLD.pid;END IF;END IF;RETURN NEW;END;$body$LANGUAGE 'plpgsql';CREATE TRIGGER "my_tree_update" BEFORE UPDATEON "public"."my_tree" FOR EACH ROWEXECUTE PROCEDURE "public"."my_tree_update_trigger"();Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
112. Управление Adjacency ListДенормализацияcounter и level:Триггер на INSERT (PostgreSQL):CREATE OR REPLACE FUNCTION "public"."my_tree_insert_trigger" ()RETURNStrigger AS$body$DECLAREparentmy_tree;BEGIN-- Что денормализовано, то ставить вручную нельзяNEW.counter := 0;NEW.level := 0;-- Если определен pid тогда обновляем счетчик родителяIFNEW.pidIS NOT NULL ORNEW.pid > 0 THEN-- Сразу вернем результат обновления родителя-- Финт ушами с CASE при обновлении счетчика из-за того что NULL + 1 = NULL, -- поэтому эти поля лучше сразу делать NOT NULL, а сейчас перестрахуемсяUPDATEmy_treeSETcounter = CASE WHEN counterIS NULL THEN 1 ELSEcounter + 1 ENDWHEREid = NEW.pidRETURNING * INTOparent;-- Проверим существование родителя, без отмены операцииIFparent.idIS NULL THENRAISE NOTICE 'ОШИБКА! Родителя с таким ID нет (%)', NEW;NEW.pid := 0;ELSE-- Устанавливаем значение level для вставляемого узлаNEW.level := CASE WHEN parent.levelIS NULL OR parent.level = 0 THEN 1 ELSENEW.level + 1 END;END IF;END IF;RETURN NEW;END;$body$LANGUAGE 'plpgsql';CREATE TRIGGER "my_tree_insert" BEFORE INSERT ON "public"."my_tree" FOR EACH ROW EXECUTE PROCEDURE "public"."my_tree_insert_trigger"();Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
113. Управление Adjacency ListДенормализацияcounter и level:Триггер на UPDATE (PostgreSQL):CREATE OR REPLACE FUNCTION "public"."my_tree_update" ()RETURNStrigger AS$body$DECLAREparentmy_tree;childmy_tree;level_skew INTEGER;pids INTEGER[];tmp_pids INTEGER[];exist_update INTEGER[];BEGIN-- Опять же для предотвращения сравнения с NULL ставим 0IFOLD.pidIS NULL THEN OLD.pid := 0; END IF;IFNEW.pidIS NULL THEN NEW.pid := 0; END IF;IFOLD.levelIS NULL THEN OLD.level := 0; END IF;-- Если произошла смена родителяIFNEW.pid <> OLD.pidTHEN-- Уменьшаем счетчик старого родителя если он естьIFOLD.pid > 0 THEN UPDATE my_treeSETcounter = counter - 1 WHEREid = OLD.pid; END IF;-- Увеличиваем счетчик нового родителя если он естьIFNEW.pid > 0 THEN UPDATEmy_treeSETcounter = COALESCE(counter, 0) + 1 WHEREid = NEW.pidRETURNING * INTOparent;-- Проверяем существование родителяIFparent.idIS NULL THENRAISE NOTICE 'ОШИБКА! Родителя с таким ID нет (%)', NEW;NEW.level := 0;NEW.pid := 0;ELSE-- Если родитель есть то устанавливаем новый уровень узлаNEW.level := COALESCE(parent.level, 0) + 1;END IF;ELSENEW.level := 0;END IF;-- Данные по перемещаемому узлу обновили, теперь следует обновить-- level потомков перемещаемого узлаlevel_skew := NEW.level - OLD.level;...Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
114. Управление Adjacency ListДенормализацияcounter и level:Триггер на UPDATE (PostgreSQL), продолжение:...-- Смещение уровня таки естьIFlevel_skew <> 0 THENpids := ARRAY[NEW.id];exist_update := ARRAY[NEW.id]; -- Пока есть узлы у которых есть потомки:WHILEpids[1] IS NOT NULL LOOPtmp_pids := ARRAY[NULL];-- Обновляем всех потомков на уровнеFORchildIN UPDATE my_treeSETlevel = level + level_skewWHEREpid = ANY (pids) ANDid <> ALL (exist_update) RETURNING *LOOP-- Поставим защиту для того, что бы невозможно было поставить узел в починение своему потомкуIFchild.id = NEW.pidTHENRAISE EXCEPTION 'Ошибка! Нельзя ставить узел в подчинение потомку! (%)', NEW;RETURN NULL;END IF;-- Если у потомка еще есть потомки, то добавляем его id в список на обновление-- следующего уровняIFchild.counterIS NOT NULL ANDchild.counter > 0 THENtmp_pids := array_prepend(child.id, tmp_pids);END IF;-- Защита от повторовexist_update := array_append(exist_update, child.id);END LOOP;pids := tmp_pids;END LOOP;END IF;END IF;RETURN NEW;END;$body$LANGUAGE 'plpgsql'; CREATE TRIGGER "my_tree_update" BEFORE UPDATEON "public"."my_tree" FOR EACH ROWEXECUTE PROCEDURE "public"."my_tree_update"();Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
115. Управление Adjacency ListДенормализацияcounter и level:Триггер на DELETE (PostgreSQL) :CREATE OR REPLACE FUNCTION "public"."my_tree_delete_trigger" ()RETURNStriggerAS$body$BEGIN-- Если есть родитель, то обновляем его счетчикIFOLD.pidIS NOT NULL AND OLD.pid > 0 THENUPDATEmy_treeSETcounter = counter – 1WHEREid = OLD.pid;END IF;IFcurrent_setting('user_vars.tree_delete') = 'levelup' THENUPDATEmy_treeSETpid = OLD.pid WHERE pid = OLD.id;ELSIFcurrent_setting('user_vars.tree_delete') = 'root' THENUPDATEmy_treeSETpid = 0WHEREpid = OLD.id;ELSEIFOLD.counterIS NOT NULL AND OLD.counter > 0 THENDELETE FROM my_treeWHEREpid = OLD.id;END IF;END IF;RETURN OLD;END;$body$LANGUAGE 'plpgsql';CREATE TRIGGER "my_tree_delete" AFTER DELETEON "public"."my_tree" FOR EACH ROWEXECUTE PROCEDURE "public"."my_tree_delete_trigger"();Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
116. Управление Adjacency ListДенормализацияcounter и level:Способы удаления ветвей:1. Транзакционный:BEGIN;SELECTset_config('user_vars.tree_delete', 'levelup', TRUE);DELETE FROM my_treeWHEREid = [item.id];COMMIT;1. Не транзакционный:DELETE FROM my_treeUSING (SELECTset_config('user_vars.tree_delete', 'root', TRUE) ) ASconfWHEREid = [item.id];Иерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)
117. Вопросы?Статьи по теме: http://doc.prototypes.ru/database/trees/Сергей ТомулевичRambler, Москва, 2010Карикатуры: Сергей КорсунИерархические структуры в реляционных базах данных: Adjacency List (Смежные вершины)