powered by simpleCommunicator - 2.0.60     © 2026 Programmizd 02
Целевая тема:
Создать новую тему:
Автор:
Закрыть
Цитировать
Форумы / PostgreSQL [игнор отключен] [закрыт для гостей] / Готовое решение для работы с деревьями.
41 сообщений из 41, показаны все 2 страниц
Готовое решение для работы с деревьями.
    #33233975
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Код: plaintext
1.
2.
3.
4.
5.
6.
7.
8.
Пожалуйста помогите проверить и доработать.
Очень нужна обработка ошибок толковая.
Замечания:
	1) после создания таблицы дерева в нее нужно вставить строчку корневого узла
	2) корневой узел может быть только один
	3) параметры x и y в процедурах вставки и перемещения обозначают
		x - слева или справа
		y - равносильный или подчиненный
Код: plaintext
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
CREATE SCHEMA "core" AUTHORIZATION "root";

CREATE TABLE "core"."tree" (
  "id" SERIAL, 
  "pid" INTEGER, 
  "lft" INTEGER NOT NULL, 
  "rgt" INTEGER NOT NULL, 
  "lvl" SMALLINT NOT NULL, 
  CONSTRAINT "PK_tree" PRIMARY KEY("id")
) WITHOUT OIDS;

CREATE INDEX "IX_tree" ON "core"."tree"
  USING btree ("lft", "rgt", "lvl");

CREATE OR REPLACE FUNCTION "core"."spTreePoint" (did integer, x boolean, y boolean) RETURNS "pg_catalog"."record" AS
$$
declare
	dr record;
	rr record;
begin
	select into dr id, pid, lft, rgt, lvl
		from "core"."tree" where id = did;
	if y then select into rr
		cast(dr.id as integer) as pid,
		cast(case when x then dr.rgt -  1  else dr.lft end as integer) as pnt,
		cast(dr.lvl +  1  as smallint) as lvl;
	else select into rr
		cast(dr.pid as integer) as pid,
		cast(case when x then dr.rgt else dr.lft -  1  end as integer) as pnt,
		cast(dr.lvl as smallint) as lvl;
	end if;
	return rr;
end;
$$
LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;

CREATE OR REPLACE FUNCTION "core"."spTreeInsert" (did integer, x boolean, y boolean) RETURNS integer AS
$$
declare
	rr record;
	nid int4;
begin
	select into rr pid, pnt, lvl from "core"."spTreePoint" (did, x, y)
		as (pid integer, pnt integer, lvl smallint);
	update "core"."tree" set rgt = rgt +  2 , lft = case when lft > rr.pnt then lft +  2  else lft end
		where rgt > rr.pnt;
	insert into "core"."tree" (pid, lft, rgt, lvl)
		values (rr.pid, rr.pnt +  1 , rr.pnt +  2 , rr.lvl);
	select into nid
		currval('core.tree_id_seq');
	return (nid);
end;
$$
LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;

CREATE OR REPLACE FUNCTION "core"."spTreeMove" (sid integer, did integer, x boolean, y boolean) RETURNS integer AS
$$
declare
	sr record;
	rr record;
	d_src integer;
	d_mov integer;
	d_lvl smallint;
begin
	select into sr lft, rgt, lvl from "core"."tree"
		where id = sid;
	select into rr pid, pnt, lvl from "core"."spTreePoint" (did, x, y)
		as (pid integer, pnt integer, lvl smallint);
	d_src := sr.rgt - sr.lft +  1 ;
	d_lvl := rr.lvl - sr.lvl;
	if rr.pnt > sr.rgt then
		d_mov := rr.pnt - sr.lft +  1  - d_src;
		update "core"."tree" set
			lft = case when rgt <= sr.rgt then lft + d_mov else case when lft > sr.rgt then lft - d_src else lft end end,
			lvl = case when rgt <= sr.rgt then lvl + d_lvl else lvl end,
			rgt = case when rgt <= sr.rgt then rgt + d_mov else case when rgt <= rr.pnt then rgt - d_src else rgt end end
		where rgt > sr.lft and lft <=  rr.pnt;
	else
		d_mov := rr.pnt - sr.lft +  1 ;
		update "core"."tree" set
			rgt = case when lft >= sr.lft then rgt + d_mov else case when rgt < sr.lft then rgt + d_src else rgt end end,
			lvl = case when lft >= sr.lft then lvl + d_lvl else lvl end,
			lft = case when lft >= sr.lft then lft + d_mov else case when lft > rr.pnt then lft + d_src else lft end end
		where rgt > rr.pnt and lft < sr.rgt;
	end if;
	update "core"."tree" set
		pid = rr.pid where id = sid;
	return  0 ;
end;
$$
LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33234218
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Код: plaintext
1.
2.
3.
4.
5.
Не знаю может я всех запутаю еще больше.
Вот  источник  основываясь на который я делал вышенаписанное.
Хотелось бы добавить еще несколько ссылок:
1.  Storing Hierarchical Data in a Database 
2.  Дерево каталогов NESTED SETS (вложенные множества) и управление им. Часть 2 
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33236971
.-.-.-.-.-
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
я с процедурами постгреса знаком понаслышке, но ИМХО - это очень мало.

Могу посоветовать запостить это в
http://www.livejournal.com/userinfo.bml?user=ru_pgsql
http://www.livejournal.com/userinfo.bml?user=ru_php
http://www.livejournal.com/userinfo.bml?user=ru_webdev

эти сообщества читают некоторые люди, интересующиейся nested sets
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33236991
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Код: plaintext
1.
2.
3.
4.
Эти процедуры для _управления_ деревом, на основе Nested Sets.
И единственное чего здесь не хватает это удаления узла :)
Предпологается что запросы на выборку построить сможет любой самостоятельно :)
За наводку спасибо, только вот у меня нет аккаунта в ЖЖ и я не знаю как этим добром пользоваться :(
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33237022
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Код: plaintext
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
Я вот счас сижу и думаю.
- А может все, как Guest, смотрят что мало процедур и думаю что это все ерунда?
Давайте немного поясню (ненавижу длинные посты):
Везде где я видел реализацию NS, функции вставки узла например, разбиты на несколько:
Слева на одном уровне
Справа на одном уровне
Слева на уровень ниже
...
То же самое с движением узла.
Причем практически во всех примерах которые я смотрел, половина вещей сделанна половина нет.
Посмотрев на это пришел к выводу, что для вставки и перемещения требуются два дополнительных параметра, которые помогут более точно задать точку назначения.
Я назвал их X и Y они оба логические (2 во второй степени = 4 хихи).
Получается что у меня четыре процедуры на вставку, и четыре процедуры на передвижение + процедура которая обслуживает координаты, и того 9.
Нужно всего лишь добавить два варианта удаления (вырезание узла, удаление с потомками). Это еще плюс две операции (но одна процедура).
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33237399
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
авторПосмотрев на это пришел к выводу, что для вставки и перемещения требуются два дополнительных параметра, которые помогут более точно задать точку назначения.
Я назвал их X и Y они оба логические (2 во второй степени = 4 хихи).

Все очень интересно, но не могли бы Вы пояснить (хотя бы на пальцах) смысл введенных логических переменных? Что значит "более точно задать точку назначения"? Это оптимизирует поиск узла?

Похоже, вложенные множества сильно оптимизируют скорость выборки из дерева, однако как зависит скорость вставки и удаления от числа узлов дерева? Эта зависимость сильно хуже линейной?

Еще один вопрос: могут ли быть пробелы в нумерации левых и правых ключей или это принципиальное условие?

P.S. Простите за обилие вопросов, это не со зла :) Мне действительно интересна эта тема.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33237428
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Код: plaintext
1.
nostromo
Все очень интересно, но не могли бы Вы пояснить (хотя бы на пальцах) смысл введенных логических переменных?
Что значит "более точно задать точку назначения"? Это оптимизирует поиск узла?


Они не улучшают время выполнения.
Они облегчают жизнь.

Исходное дерево:

Корень
Птицы
Звери
Лев
Растения

Вставка (Звери, X = 0, Y = 0) - _до_ Зверей на _том_же_уровне_

Корень
Птицы
НОВЫЙ
Звери
Лев
Растения

Вставка (Звери, X = 0, Y = 1) - _до_ _дочерних_ элементов Зверей

Корень
Птицы
Звери
НОВЫЙ
Лев
Растения

Вставка (Звери, X = 1, Y = 0) - _после_ Зверей на _том_же_уровне_

Корень
Птицы
Звери
Лев
НОВЫЙ
Растения

Вставка (Звери, X = 1, Y = 1) - _после_ _дочерних_ элементов Зверей

Корень
Птицы
Звери
Лев
Новый
Растения

Видите зависимость между значениями X,Y и подчеркнутыми фразами?
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33237431
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
nostromo
Похоже, вложенные множества сильно оптимизируют скорость выборки из дерева, однако как зависит скорость вставки и удаления от числа узлов дерева? Эта зависимость сильно хуже линейной?


Насчет скорости обновления все надо проверять.
Точных данных пока не могу сообщить.

nostromo
Еще один вопрос: могут ли быть пробелы в нумерации левых и правых ключей или это принципиальное условие?


Мой вариант не допускает пробелов по идее ;)
Пробелы мешают быстро считать кол-во дочерних элементов вот таким запросом:

Код: plaintext
SELECT (rgt-lft- 1 )/ 2  as childs_count FROM tree_table

В результате этого запроса вы получите кол-во детей для каждого узла.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33237448
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Спасибо. Функционально назначение переменных X и Y понятно.

Однако, с точки зрения употредления деревьев понятие "естественного порядка дочерних элементов", как правило, не важно. Например, вложенные файлы и папки мы сортируем каким-то внешним условием: по именам в алфавитном порядке, по дате и т.д. В редких случаях можно признать полезность естественного порядка, но его всегда можно реализовать как дополнительное поле узла дерева.

Вопрос про эффективность Вашего решения я задал потому, что много слышал о соответствующих проблемах реализации иерархических структур поверх реляционных БД.
Вопрос о пробелах в нумерации ключей для обхода связан с этим же: с ростом дерева расходы на обновление ключей при вставке и удалении узлов очевидно будут расти. Если разрешить пробелы в нумерации, то, как мне кажется, можно будет большинство вставок и удалений узлов без обшироных обновлений ключей. Когда-то давно строки программы на Бейсике нумеровались числами, кратными 10, чтобы удобнее было вставлять новые в случае необходимости.

Теперь по-поводу легкого подсчета количества дочерних элементов. Судить о том, что важнее -- эффективность модификаций дерева или легкость подсчета числа его элементов можно, на мой взгляд только исходя из конкретики прикладной задачи и результатов контрольных тестов на данных, близких к реальным.
Можно конечно придумывать лекарство, а потом искать болезнь, но в такой сугубо прикладной области, как базы данных, как мне кажется, более естественен обратный путь.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238191
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Nostromo, если честно, не хочется полемизировать.
Данное решение было сделанно мной под конкретную задачу, такая задача стоит перед многими. В моем случае это создание удобной навигации по веб-каталагу с несколькими тысячами позиций.

Очень хочется добавить, что на мой взгляд иерархии в виду сложности представления в реляционном виде подходят только для определенного круга задачь. Решения данных задач зависят от средств реализации. Мне крайне сложно представить задачу в которой будет применяться ЧАСТО обновляемая иерархия, в таком случае ее просто не стоит использовать помоему. Возьмите к примеру веб-сайт, у которого несколько десятков тысяч уникальных хостов в сутки, и несколько сотен тысяч хитов, как вы себе представляете более частное использование вставки и обновления чем выборки в данном случае, эти величины просто несопоставимы, т.к. разница в несколько порядков минимум.

Насчет бейсика и пропусков. Можно придумать огромное кол-во фишечек, например ввести такое понятие как номер дерева, если у вас их несколько, ну и мало ли что еще. Но все это частные решения, которые мне в данном случае не интерестны, я хотел бы обсудить КОНКРЕТНОЕ решение САМОЙ идеи, без.

Этот топик был содан для того чтобы мне специалисты по постгресу помогли доработать мои недостатки, и для того чтобы у людей была конкретно реализованная идея, которую можно переработать под себя, согласитесь это совсем несложно было бы сделать.

P.S> Я не хотел бы счас спорить о целесообразности или наоборот данного решения. Я хотел бы найти ошибки и исправить их, оставив качественно проработанный результат, для дальнейшего использования.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238353
LeXa NalBat
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Paul Chabinsky, вы смотрели имеющиеся решения? http://sql.ru/forum/actualthread.aspx?tid=202006#1725807
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238413
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Paul Chabinsky P.S> Я не хотел бы счас спорить о целесообразности или наоборот данного решения. Я хотел бы найти ошибки и исправить их, оставив качественно проработанный результат, для дальнейшего использования.
гм. Решение представления дерева в виде хранения номеров правых и левых узлов не ваше. Т.о. обсуждать его тут незачем. (Если не обсуждать его эффективность с т.з конкретных применений, чего вы не хотите). Вы хотите чтобы тут вылавливали ваших блошек? Кому это надо?

Я себе реализовал другое решение (из за того, что моя задача - это интенсивно растущее _преимущественно_ вверх (т.е. с _редкой_ пересадкой ветвей) дерево. При этом Ваше "право/левое" решение в среднем будет двигать полдерева (обновлять номера соседних узлов) - что затратно.

Очевидно, что хранение только родительского узла наиболее дешево по вставке, но видимо далеко не дешево по поиску иерархического подчинения (через вызов рекурсивной ф-ии). Но, насколько я помню, и "ваше" решение сводит к _агрегатам_ относительно частые задачи поиска (т.е. медленно, или я не прав?). Могу обсуждать преимущества моего решения против вашего, но ни ловить ваши ошибки реализации, ни предлагать для ловли блошек (того же рода) свои не собираюсь.


ЗЫ. Кстати сказать, к постгресу есть и _патчик_, который позволяет создавать рекурсивные запросы к деревьям. Где-то тут пробегали люди, его применявшие.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238468
glebofff
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238625
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
2 LeXa NalBat
Видел ссылку что вы предложили. Но ничего конкретного для себя в ней не нашел к сожалению.

2 4321
1) Я нивкоем случае не претендую на то что я придумал как хранить дерево. И я да же не претендую на то что придумал как им управлять, я просто сделал аккуратную и лаконичную на мой взгляд реализацию. Которую хотел обсудить.
2) Joe Celko (его я считаю автором данного алгоритма) написал книжку про деревья в базе, я ее к сожалению не видил. Но в интернете именно от его имени не встречается ниодной реализации управления таким деревом, единственное что я нашел это процедура которая из id и parent_id, расчитывает поля left и right всего дерева.
3) Про агрегаты не понял к сожалению. Но насчет поиска могу только привести пример выборки дочерних узлов ноды:
Код: plaintext
SELECT * FROM Tree WHERE left > parent_left AND right < parent_right AND level > parent_level
Ну сравните с рекурсией сами...
4) Насчет коннект бай, это та же самая рекурсия... просто враппер для того чтоб вам не писать рекурсивную функцию самому вручную.

2 glebofff
Спасибо за ссылку, ее я то же видил, до того как сделал выложенный тут вариант. Мне просто данный вариант кажется несколько несистематезированным.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238690
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Paul Chabinskyпривести пример выборки дочерних узлов ноды:
Код: plaintext
SELECT * FROM Tree WHERE left > parent_left AND right < parent_right AND level > parent_level

кааца я вспомнил (поправьте если совру):
- тут у вас AND level > parent_level - лишнее (для правильного обхода по дереву левел есть ф-я лефт и райт)
и кажется само хранение левела - избыточно, а левел как раз и считается при помощи каунтов по неким запросам для "минимально избыточного" древа, хранящего только лефт и райт.


_
зы: я не работал с рекурсией. А именно со вспомогательной индексированной таблицей "полной структуры" дерева (все ссылки с узла на всех его предков по иерархии, без указания левела). Оценочно (число записей в такой структуре) <= (число записей в дереве)*(глубина дерева). Структура может строиться по иерархическому дереву в любой момент, но у меня строится триггерами синхронно. Количество вставляемых/удаляемых в структуру записей при вставке/удалении узла в конец/из конца ветки <=(глубина дерева) - как правило это много меньше половины узлов дерева - как в вашем случае; при перемещении/удалении целых ветвей естественно затрагивается гораздо больший пласт записей, ну дык это и более редкая операция.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238802
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
2 4321
Да действительно поля parent_id и level избыточны, но значительно упрощают работу. Если они не нужны можно легко удалить их обработку.

С вашим методом я знаком, пробовал его, очень хароший метод, но мне он не подходит.

Обсудить я хотел бы именно Nested Set, и конкретную реализацию (устал повторять).

P.S> Вообще прихожу к выводу что я зря все это затеял.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33238841
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Paul Chabinsky2 4321
Да действительно поля parent_id и level избыточны, но значительно упрощают работу. Если они не нужны можно легко удалить их обработку.

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

Paul ChabinskyОбсудить я хотел бы именно Nested Set, и конкретную реализацию (устал повторять).
P.S> Вообще прихожу к выводу что я зря все это затеял. ничего странного. Если найдется еще реализатор/пользователь _именно_ Nested Set, он видимо и поковыряется именно в _реализации_. Не расстраивайтесь. Я долго зарился на NS, но число узлов >> 10 000, знаете ли. Стоимость апдейта в среднем >>5000 записей _самого_ дерева на каждую операцию вставки не вдохновляла.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33239621
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
To 4321:

4321зы: я не работал с рекурсией. А именно со вспомогательной индексированной таблицей "полной структуры" дерева (все ссылки с узла на всех его предков по иерархии, без указания левела). Оценочно (число записей в такой структуре) <= (число записей в дереве)*(глубина дерева). Структура может строиться по иерархическому дереву в любой момент, но у меня строится триггерами синхронно.

Если я правильно понял, то "индексированной таблицей "полной структуры"" -- это таблица, каждая запись которого соответствует ребру графа, если рассматривать дерево как граф. Если корень только 1, то количество ребер в точности равно N-1, где N -- количество количество узлов дерева (есть такая теорема). Если граф состоит из M деревьев и имеет N вершин, то соответственно в нем будет ровно N-M ребер.

По поводу хранения идеи реализации произвольного графа. Все задачи, связанные с графами можно разделить на два класса: 1) задачи, в которых узлы представляют достаточно сложные прикладные объекты. В этом случае граф имеет вспомогательную роль и его можно рассматривать как один из индексов для поиска; 2) задачи, в которых важна именно структура графа. В первую очередь, к ним относятся классические сетевые задачи из методов оптимизации (алгоритм Форда-Фолкерсона, алгоритм Краскала, поиск подграфов специального вида и т.д.).
Насколько я знаю, для задач второго сорта наиболее эффективными структурами представления графа являются матрица смежности и матрица инцедентности, т.е. БД имеет смысл использовать только для больших графов, да и то для хранения именно таких структур. В задачах первого сорта, по моим наблюдениям, графы сложнее деревьев встречаются редко (могу только векторные редакторы вспомнить). Впрочем, это лишь некоторое мое замечание, ничего против идеи я не имею.

To Paul Chabinsky:
Paul ChabinskyВозьмите к примеру веб-сайт, у которого несколько десятков тысяч уникальных хостов в сутки, и несколько сотен тысяч хитов, как вы себе представляете более частное использование вставки и обновления чем выборки в данном случае, эти величины просто несопоставимы, т.к. разница в несколько порядков минимум.
Не может ли случиться, что наилучшим решением в этом случае будет являться использование встроенного btree индекса, который, как я понимаю, и есть дерево для поиска, реализованное на низком уровне?
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33239698
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
блин, авторизовался, писал подробный ответ, старался высунув язык, как идиот, а этот долбанный сайт сожрал всё, выдал отлуп (куку он поселя, видимо) и не поперхнулся ... сцука

короче вы ,nostromo, несолько неправы. Что такое вспомогательная структура - см по ссылке автора в посте '1825061'.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33239770
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
To 4321:
Да, просмотрел я эту ссылку, теперь понятно. Впрочем, теперь можно сделать новые оценки (ну математик я по профессии :)). Если имеем сбалансированное дерево c L уровнями, у которого каждый узел имеет k предков, то количество узлов (записей в первой таблице) будет равно: (k^L-1)/(k-1), а количество записей во второй, вспомогательной таблице: ((L-1)*k^(L+1) - L*k^L + k) / (k-1)^2.

Сама идея с двумя таблицами мне понравилась, может пригодиться.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33240323
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Меня спрашивали насчет произоводительности.

Компутер: Ноутбук, 1.3Мц, 512мб.

Я сделал дерево на 6к элементов (это конечно оч мало, но меня устраивает).
Корневой элемент содержить 10-ть дочерних (нулевой уровень),
Элементы первого еще по 10-ть
Элементы второго еще по 10-ть
Эдлементы третьего по 5-ть.

Создание такова дерева заняло 110 секунд (скриптом на PHP).

Не спрашивайте как получилось 6к

Так вот вставка узла в середину (как дочерний для среднего элемента четвертого уровня) дерева : 200-220мс
Вставка узла в начало первого уровня 400-500 мс.
Перемещение среднего узла с его потомками в начало первого уровня (хотя я мож че попутал) : 10мс.

Меня такие результаты устраивают.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33240334
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
nostromo(ну математик я по профессии :)).Внушает! nostromoЕсли имеем сбалансированное дерево c L уровнями, у которого каждый узел имеет k предков, то количество узлов (записей в первой таблице) будет равно: (k^L-1)/(k-1), а количество записей во второй, вспомогательной таблице: ((L-1)*k^(L+1) - L*k^L + k) / (k-1)^2.долго думал. но раз есть что-то про "балананец" подумал, что речь таки о потомках, а не о предках. (каждый узел имеет не более 1 прямого предка и не более L иерархических), причем даже для случая "потомков" надо подправить формулировку -"каждый узел, за исключением терминальных... " ( имеет ). (кстати, а к какому типу девиаций относится имение предков, а к какому - потомков?)

Но вообще-то говоря интересны именно деревья (и графы) количество потомков у узла которых заведомо не фиксировано, и более того, зависит от узла (некие классификаторы сущностей с перестраиваемой классификацией). Т.ч. в чести более мягкие ограничения и более мягкие оценки.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33241441
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Да, конечно, в моем последнем посте есть ошибки, должно быть так:

Если имеем сбалансированное дерево c L уровнями (полностью заполненными) , у которого каждый узел, кроме терминальных (не имеющих потомков) имеет k потомков , то количество узлов (записей в первой таблице) будет равно: (k^L-1)/(k-1), а количество записей во второй, вспомогательной таблице: ((L-1)*k^(L+1) - L*k^L + k) / (k-1)^2.

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

Далее, предположим, что L фиксировано, а k равстет, тогда количество вспомогательных записей эквивалентно L*k^(L-1). Если рассматривать рост количества записей доп. таблицы (обозначим через M количество записей доп. таблицы) относительно количества узлов дерева (обозначим через N), то получим оценку: M/N эквивалентно L-1 (при больших k). Т.о., для дерева с четырьмя уровнями иерархии, L=4 (на первом уровне находится только корневой узел), у которого каждый узел (кроме терминальных) имеет в среднем 10 потомков, можно ожидать, что количество строк доп. таблицы будет в 3 раза превышать количество узлов (в этом случае количество узлов N = 1111 в среднем).

В случае, когда k фиксировано, а L растет имеем: M/N эквивалентно (L-1)*k/(k-1).

Замечу, что эти оценки асимптотические (т.е. понятие больших значений не конкретизируется), и справедливы в среднем для всех деревьев с указанными ограничениями.

Аналогично можно рассмотреть случай, когда среднее количество потомков узла зависит от увовня (и эта зависимость известна), а также случай, когда имеется не одно дерево, а лес.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33241444
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Предлагаю сравнить подходы с Nested Sets (NS) и доп. таблицей (ДТ) by 4321.

То, что приведено ниже, только мое мнение, на котором я сильно не настаиваю!

Типичные задачи:
1. получить всех предков определенного элемента.
NS - скорость пропорциональна глубине (необходим рекурсивный поиск,
поле level помогает слабо)
ДТ - быстрый запрос по доп. таблице

2. получить всех потомков определенного элемента.
NS - быстрый запрос
ДТ - быстрый запрос по доп. таблице

3. получить количество потомков данного элемента
NS - мгновенно
ДТ - быстрый запрос по доп. таблице

3. получить непосредственных потомков данного элемента
одинаково быстрый запрос для обоих подходов

4. получить информацию об уровне заданного элемента
NS - мгновенно
ДТ - быстрый запрос по доп. таблице (считаем предков)

5. проверка терминальности элемента
одинаково быстрый запрос для обоих подходов

6. поиск всех элементов заданного уровня
NS - быстрый запрос
ДТ - довольно тяжелый запрос (рекурсивный поиск от корня)

Особенности NS:
- сравнимельно маленькая избыточность данных
- поддержка и управление линейной упорядоченности непосредственных потомков
- довольно тяжелые обновления при модификации дерева.

Особенности ДТ:
- легкая модификация дерева
- приличная избыточность данных (вся доп. таблица)
- при слишком больших размерах доп. таблицы, количество записей в которой больше числа узлов, "быстрые" запросы могут оказаться не такими уж и быстрыми
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33241582
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
анализ (в части "рекурсий") не совсем верен:
к 1. если я правильно помню, то никаких рекурсий в НС не нужно, хвататет групповых запросов с агрегированием. Сейчас вспоминать не хочу, что именно нужно для 1. но можно поискать ссылки по н.с.
то же самое к
6. t_parents - доп таблица
автор
Код: plaintext
1.
2.
3.
SELECT l.id, ((count(l.pid ) -  1  ))::smallint AS "level" 
   FROM t_parents l
  GROUP BY l.id
HAVING ((count(l.pid ) -  1  ))::smallint =  5 ;
Суммарное время выполнения запроса:500 ms.
Время получения данных:109 ms.
получено строк: 2996
на большем объеме данных
Код: plaintext
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
-- Выполнение запроса:
SELECT l.id, ((count(l.pid ) -  1  ))::smallint AS "level" 
   FROM t_parents l
  GROUP BY l.id
HAVING ((count(l.pid ) -  1  ))::smallint =  5 ;

--Суммарное время выполнения запроса:3734 ms.
--Время получения данных:16 ms.
--Всего строк: 21311.
--строк не извлечено: 21211.
Select count(*) From tree;
 44565 
При этом никто не мешает мне хранить левел явным образом в собственно таблице дерева.
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33241626
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
- приличная избыточность данных (вся доп. таблица)- ее б скажем не хранить, а стартовать в общую "переменную" табличного вида (обобщенную между коннектами времянку) при первом запросе к базе любого коннекта, и нехай себе висит (и перестраивается) только в памяти (размер то записи невелик). Может быть есть что-то в этом ключе?
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33241650
Paul Chabinsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
1. получить всех предков определенного элемента.
Код: plaintext
SELECT * FROM tree WHERE left <= $left AND right >= $right ORDER BY left
Рекурсии не нужны
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33241711
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Paul Chabinsky1. получить всех предков определенного элемента.
Код: plaintext
SELECT * FROM tree WHERE left <= $left AND right >= $right ORDER BY left
Рекурсии не нужнызначит помню правильно :)


Да, кстати:
Код: plaintext
1.
WHERE left > parent_left AND right < parent_right;
WHERE left <= $left AND right >= $right;  
если я правильно понимаю, это довольно неприятные условия для индекса (left,right) ? Видимо по лефт идет поиск по индексу, а по райт - уже фильтр?
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33241712
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Кажется, если таблица в памяти мало места занимает, то на диске и подавно :)

Замечу, что в постгресе каждая строка таблицы сопровождается заголовком приличного размера, частенько превышающего размер пользовательских данных:
из документацииAll table rows are structured in the same way. There is a fixed-size header (occupying 27 bytes on most machines), followed by an optional null bitmap, an optional object ID field, and the user data. The header is detailed in Table 49-4. The actual user data (columns of the row) begins at the offset indicated by t_hoff, which must always be a multiple of the MAXALIGN distance for the platform...
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33242741
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
nostromoКажется, если таблица в памяти мало места занимает, то на диске и подавно :)смысел не в месте в памяти, а в соотношениии цен дисковых операций к операциям в памяти. ( т.е. чисто физические 3-4 порядка -прим. для математиков именно этим кстати кажеца объяснялося модное стартование tempdb в память для мс-скуль 6.5 ) А поскольку все данные этой таблицы - исчисляемые, (изьыточные) - то и нет необходимости хранить их на диске (достаточно строить, но быстро и один раз, после старта)
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33242786
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
да, а по поводу хидеров - это кажеца повод хранить исчисляемую (избыточную, но необходимую для SQL выборок) иерархию в индексированных тестовых полях в виде .1.123.78987... - кол-во шапок на иерархию узла поджимаеца (даже если табличка иерархий будет отдельной от соб-сно дерева), правда апдейты пойдут с расщеплением поля, но, похоже, не сложным. (И еще - не потребуется ли обращенного индекса - т.е. (функционального?) индекса на обращенную строку?)
Что вы об этом думаете?
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33243532
nostromo
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
4321смысел не в месте в памяти, а в соотношениии цен дисковых операций к операциям в памяти. (т.е. чисто физические 3-4 порядка
хорошо, согласен, однако такие оптимизации в проектах обычно проводятся в последнюю очередь, к тому же так можно что угодно ускорить, а не только Ваш алгоритм.

4321да, а по поводу хидеров - это кажеца повод хранить исчисляемую (избыточную, но необходимую для SQL выборок) иерархию в индексированных тестовых полях в виде .1.123.78987... - кол-во шапок на иерархию узла поджимаеца (даже если табличка иерархий будет отдельной от соб-сно дерева), правда апдейты пойдут с расщеплением поля, но, похоже, не сложным.
Честно говоря, работать с индексированными полями не приходилось, не знаю насколько это быстро. Память, конечно, экономится, но потерять в скорости, как мне кажется, можно прилично. Например, перевод из текста в число операция не самая быстрая, да и лишняя, т.к. память экономится только для маленьких значений (которых меньшинство): 4-х байтовое целое занимает до 10 байтов в десятичной десятичной и до 8 в шестнадцатеричной. В общем, здесь лучше индексированные целые поля.

4321(И еще - не потребуется ли обращенного индекса - т.е. (функционального?) индекса на обращенную строку?)
Это не понял, поясните.

Если мыслить масштабно, то, кажется, идеальный вариант для любого алгоритма, оптимизирующего работу с некторой структурой -- это соответствующий встроенный в систему индекс плюс оптимизатор, умеющий его эффективно использовать. Может, пора связываться с разработчиками или писать свое расширение? :)
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33243750
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
nostromo Например, перевод из текста в число операция не самая быстрая ну в случае хранения иерархий в поле (hierarchy text;) запросы на вхождение будут иметь (скажем) вид
WHERE c.hierarchy LIKE t.hierarchy||'.*'
т.ч. преобразование типов тут не нужно. (оно потребуется только при заполнении hierarchy триггером - из числа в текст)

nostromo т.к. память экономится только для маленьких значений (которых меньшинство): 4-х байтовое целое занимает до 10 байтов в десятичной десятичной и до 8 в шестнадцатеричной. речь шла о экономии на шапках записей - но кажется вы о том, что потери из-за преобразования в строку их съедят?

nostromo 4321(И еще - не потребуется ли ... индекса на обращенную строку?)
Это не понял, поясните.мои извинения, - это ложный перенос идеи о 2-х индексах (pid,id)|(id,pid) (что удобно для поиска как предков так и потомков) на случай текстовой иерархии. Кажется индекс на обращенную строку не пригодится - предки ищутся так же как и потомки только таблицы меняются в LIKE местами.



_______
ЗЗЫ:
nostromoЕсли мыслить масштабно, то, кажется, идеальный вариант для любого алгоритма, оптимизирующего работу с некторой структурой -- это соответствующий встроенный в систему индекс плюс оптимизатор, умеющий его эффективно использовать. Может, пора связываться с разработчиками или писать свое расширение? :)а я вот не думаю, что тут пора "мыслить масштабно". Ибо...
Ибо обнаружил, что идея спотыкается на том, что при апдейте (пересадке веток) приходится прибегать к использованию конструкций вида
DELETE ... WHERE t.field IN(SELECT f FROM ....)
(или (t.f1,t.f2) IN( SELECT f1,f2 FROM....)
, а они даже для случая отсутствия зависимости сабселекта от полей внешних (к сабселекту) считаются неоптимально - а именно сабселект считается для каждой записи (я даже пытался подоткнуть туда STABLE ф-ю -думая обмануть природу - ан. фих). Т.ч. экономия метода частична - вставка веток - дело практически "бесплатное" (недорогое), но вот пересадка (даже терминалов, если не выделять их пересадку в отдельную ветку триггерного алгоритма) напрягает.

я конечно нашел, что можно обмануть эту проблему (почти на порядок - полтора на ~100 000 записях в иерархичке) составлением стрингов для IN(,,,,), но это, мне кажеца, "не дело" - неужто оптимизатору так трудно выяснить отсутствие зависимости сабселекта от полей выбираемой в селекте записи? Вот уж что действительно давно пора бы было от"оптимизировать" разработчикам...
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33245121
LeXa NalBat
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
4321 Paul Chabinsky1. получить всех предков определенного элемента.
Код: plaintext
SELECT * FROM tree WHERE left <= $left AND right >= $right ORDER BY left
если я правильно понимаю, это довольно неприятные условия для индекса (left,right) ? Видимо по лефт идет поиск по индексу, а по райт - уже фильтр?Да уж, прочесывать пол-дерева ради поиска нескольких элементов (родителей) кажется неэффективным.

4321это кажеца повод хранить исчисляемую (избыточную, но необходимую для SQL выборок) иерархию в индексированных тестовых полях в виде .1.123.78987...Вроде бы так сделано в ltree.

4321DELETE ... WHERE t.field IN(SELECT f FROM ....)Можно заменить на exists ( select 1 from ... where f=t.field ). Или приджоинить: http://www.postgresql.org/docs/8.0/static/sql-delete.html#AEN41779
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33245214
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
LeXa NalBat 4321DELETE ... WHERE t.field IN(SELECT f FROM ....)Можно заменить на exists ( select 1 from ... where f=t.field ). Или приджоинить: http://www.postgresql.org/docs/8.0/static/sql-delete.html#AEN41779Спасибо, посмотрю!
( Я как раз и хотел поругаца на отсутствие "ДИЛЕТА ... ФРОМ ДЖОНА" (як скажем в неких настольных вструментах типа аксеса). Тут, насколько я понял, омиттед фром в полной красе. Вот только дилет форм аутер джон так кажеца не реализуеца.)

Что касаеца exists - пробовал в первую голову, получил времена на полпорядка _хуже_, чем при 2-х id IN(Select ...) AND pid IN(Select ...). Пока (из опробованного) наискорейшее - сшивать строку IN(,,,,,) и выполнять EXECUTE (при малом количесве подчиненных срабатывает раз в 10 -15 быстрее чем с IN(SELECT ...), но не совсем понятно, что будет при очень больших строках (есть ведь наверное и ограничения на длину SQL инструкции? Пока проверял - при перемещении веток с ~280 потомками по иерархии работает).
Правда появилась идея (не сшивать строку) а напрямую делетить записи в plpgsql-ой ХП в цикле (в аксессе бы подобное в VBA делать не стал - заведомо плохо, но в plpgsql имеет смысл оттестировать для пробы).
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33245465
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
, сорри, не заметил: also it cannot handle self-joins. т.ч. джойн по омиттед фром нервенно курит у сторонке (мля)
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33245473
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
4321, сорри, не заметил: also it cannot handle self-joins. т.ч. джойн по омиттед фром нервенно курит у сторонке (мля)Фихвам! Абманём за счет вью! (голь на видумки хитра)
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33246091
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
2 LeXa NalBat
к проблеме DELETE FROM ... WHERE IN(... ) на моих данных:

к сожалению нагрузка на сервер непостоянна. Т.ч. тестовые результаты различаются иногда на порядки по временам (на одних и тех же данных). Поэтому числа не привожу. Но примерно такое резюме (при подмене функций в триггере обновления предка узла дерева):

1.EXECUTE (... IN (\' ||с_подстановкой_наборов||\' ) ~ на порядок лучше изначального дилета.

2. цикл DELETE внутри лупа - близко по скорости к дилету по 1. (на ~ 280*3 записей в лупе). (очень может быть, что скорости дилетов по пп. 1. и 2. разнятся сильнее - в триггере есть еще и инсерты, дающие примерно такой же вклад, но радует хотя бы то, что не надо думать о предельной длине SQL строки)

3. дилет с "ommited from" джойном в WHERE с 2 видами (той же таблицы) - раза в 2-3 похуже 1 и 2

4. Очень хотелося неявно заджойнится на SETOF ф-ю. Но, кажется этого нельзя. Т.е. нельзя ни заполнить сетоф переменную в plpgsql, ни написать
функция($1).поле в WHERE. Можно видимо посмотреть в сторону времянки. Но времянки - это к сожалению не фонтан (дисковые операции).

ЗЫ: надо бы потестить чистые дилеты на модельных данных, на незагруженном сервере. неушто придеца дома
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33246188
LeXa NalBat
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
43213. дилет с "ommited from" джойном в WHERE с 2 видами (той же таблицы) - раза в 2-3 похуже 1 и 2киньте пожалуйста explain
...
Рейтинг: 0 / 0
Готовое решение для работы с деревьями.
    #33246301
4321
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
LeXa NalBat 43213. дилет с "ommited from" джойном в WHERE с 2 видами (той же таблицы) - раза в 2-3 похуже 1 и 2киньте пожалуйста explain как говорилося выше:(при подмене функций в триггере обновления предка узла дерева)где для случая джойна подставлялась:
Код: plaintext
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
CREATE OR REPLACE FUNCTION public.f_delete_inherited_hierarchy_join(int4)
  RETURNS bool AS
'DECLARE
BEGIN
DELETE FROM t_parents 
	WHERE t_parents.pid = v_parents.pid 
		AND  v_parents.id = $1 
		AND t_parents.id = v_parents_.id		
		AND v_parents_.pid = $1;	
	
RETURN TRUE;
END;
'
  LANGUAGE 'plpgsql' STABLE STRICT;
для предка 74 имеется 3 прямых и 280 иерархических потомков
Код: plaintext
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
EXPLAIN ANALYZE DELETE FROM t_parents 
	WHERE t_parents.pid = v_parents.pid 
		AND  v_parents.id =  74  
		AND t_parents.id = v_parents_.id		
		AND v_parents_.pid =  74 ;	
QUERY PLAN
Merge Join  (cost= 11114 . 69 .. 11138 . 48  rows= 808  width= 22 ) (actual time= 105 . 38 .. 105 . 38  rows= 0  loops= 1 )
"  Merge Cond: (""outer"".pid = ""inner"".pid)"
  ->  Sort  (cost= 11085 . 65 .. 11092 . 47  rows= 2730  width= 18 ) (actual time= 105 . 16 .. 105 . 16  rows= 1  loops= 1 )
        Sort Key: public.t_parents.pid
        ->  Nested Loop  (cost= 0 . 00 .. 10929 . 83  rows= 2730  width= 18 ) (actual time= 0 . 40 .. 100 . 84  rows= 831  loops= 1 )
              ->  Index Scan using pid_id on t_parents  (cost= 0 . 00 .. 959 . 86  rows= 341  width= 4 ) (actual time= 0 . 18 .. 5 . 00  rows= 280  loops= 1 )
                    Index Cond: (pid =  74 )
              ->  Index Scan using t_parents_pkey on t_parents  (cost= 0 . 00 .. 29 . 14  rows= 7  width= 14 ) (actual time= 0 . 11 .. 0 . 30  rows= 3  loops= 280 )
"                    Index Cond: (t_parents.id = ""outer"".id)"
  ->  Sort  (cost= 29 . 05 .. 29 . 06  rows= 7  width= 4 ) (actual time= 0 . 17 .. 0 . 17  rows= 1  loops= 1 )
        Sort Key: public.t_parents.pid
        ->  Index Scan using t_parents_pkey on t_parents  (cost= 0 . 00 .. 28 . 95  rows= 7  width= 4 ) (actual time= 0 . 08 .. 0 . 09  rows= 1  loops= 1 )
              Index Cond: (id =  74 )
Total runtime:  105 . 81  msec
...
Рейтинг: 0 / 0
Период между сообщениями больше года.
Готовое решение для работы с деревьями.
    #35029927
MySQLCraft
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
А что если ветка переедет на другой парент или промежуточные паренты вставить или удалить...
Потребуется обновление и переиндексация всех дочерних записей, а их в данной задаче в сотни раз больше чем парентов.

Интересно, что получилось в результате? Насколько работоспособна эта база и в какой предметной области применяется?
...
Рейтинг: 0 / 0
41 сообщений из 41, показаны все 2 страниц
Форумы / PostgreSQL [игнор отключен] [закрыт для гостей] / Готовое решение для работы с деревьями.
Найденые пользователи ...
Разблокировать пользователей ...
Читали форум (0):
Пользователи онлайн (0):
x
x
Закрыть


Просмотр
0 / 0
Close
Debug Console [Select Text]