Модераторы: skyboy, MoLeX, Aliance, ksnk
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Работа с MySQL. Деревья 
:(
    Опции темы
DezmASter
Дата 22.4.2007, 15:10 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Дизайнер :)
***


Профиль
Группа: Участник
Сообщений: 1520
Регистрация: 3.2.2006
Где: Украина, Запорожь е

Репутация: нет
Всего: 109



Необходимость вывода данных структурированных в форме деревьев возникает при написании собственного форума или каталога сайтов. Готовых каталогов и форумов в сети можно найти предостаточно, однако иногда чужое в готовом не годится, а переделывать написанное другим займёт гораздо больше времени, чем написать своё.

Структуру данных лучше взять общепринятую - в записи сообщения или рубрики форума содержится идентификатор родителя. Для организации вывода дерева напрашивается рекурсивная функция. Именно так сделано в Phorum'е [http://phorum.org]. Файл include/multi-threads.php содержит функцию thread, которая строит вызывается для каждого корневого сообщения и рекурсивно вызывает себя для ответов на них:

Код

function thread ($seed = 0) {
...

    if(@is_array($messages[$seed]["replies"])) {
        $count = count($messages[$seed]["replies"]);
        for($x = 1;$ x <= $count; $x++) {
            $key = key($messages[$seed]["replies"]);
            thread ($key);
            next ($messages[$seed]["replies"]);
        }
    }
}


Но вызов рекурсивной функции при выводе вызывает у меня сомнения: повторять построение дерева сообщений при каждом выводе нерационально. Структура дерева меняется только при добавлении, изменении и удалении сообщений. Данную процедуру лучше было бы вызывать при таких действиях, хранить структуру в таблице и при выводе дерева не делать никаких вычислений.

Для построения дерева достаточно знать последовательность вывода рубрик и их уровень в дереве. Добавим два поля с этими данными в таблицу: level (TINYINT(4). 127 уровней - хватит?) и sortorder (VARCHAR(128)).

Всё, что нам нужно для построения дерева - это идентификатор рубрики и её родителя. Допустим, мы имеем в каталоге несколько рубрик такого содержания:

Код

---------
id parent
---------
 3      0
 5      0
 7      0
10      3
11      7
12      5
13      3
16     10
21     16
26     11
30      3
47      7
60     10
73     13
75     47
---------


Структура дерева, подобие которой мы хотим получить такова:

Код

o- 3
|
+-o- 10
| |
| +-o- 16
| | |
| | +-o- 21
| |
| +-o- 60
|
+-o- 13
| |
| +-o- 73
|
+-o- 30

o- 5
|
+-o- 12

o- 7
|
+-o- 11
| |
| +-o- 26
|
+-o- 47
  |
  +-o- 75


Правда, данный алгоритм позволит нарисовать дерево, но без веток виде линий, как сделано на этом рисунке. Структура дерева будет нарисована при помощи отступов слева.

Вернёмся ещё раз к таблице id-parent. Это рубрики, уже отсортированные по некоторому признаку, по которому мы хотим сортировать элементы одинакового уровня. Например, по убыванию числа сайтов. Кроме id и родительской рубрики мы знаем и номер каждой из них в данном списке. Выровняем эти номера до нужной длины, добавив слева нули. После этого для каждой рубрики сделаем текстовую строку с номерами всех её родителей от самого корня:

Код

------------------------------
id sort parent sortorder level
------------------------------
 3    1      0 01            0
 5    2      0 02            0
 7    3      0 03            0
10    4      3 0104          1
11    5      7 0305          1
12    6      5 0206          1
13    7      3 0107          1
16    8     10 010408        2
21    9     16 01040809      3
26   10     11 030510        2
30   11      3 0111          1
47   12      7 0312          1
60   13     10 010413        2
73   14     13 010714        2
75   15     47 031215        2
------------------------------


При сортировке по полю sortorder мы получим именно то, что нам нужно:

Код

------------------------------
id sort parent sortorder level
------------------------------
 3    1      0 01            0
10    4      3 0104          1
16    8     10 010408        2
21    9     16 01040809      3
60   13     10 010413        2
13    7      3 0107          1
73   14     13 010714        2
30   11      3 0111          1
 5    2      0 02            0
12    6      5 0206          1
 7    3      0 03            0
11    5      7 0305          1
26   10     11 030510        2
47   12      7 0312          1
75   15     47 031215        2
------------------------------


Отступ слева делается, учитывая поле level.

Важно так же отметить, что нам не нужно ничего сортировать в самом скрипте. Для формирования полей sortorder и level нужно заблокировать таблицу от записи (чтобы не произошло изменения структуры веток), выбрать из базы идентификатор рубрики и её родителя, отсортировав по нужному признаку, и записать их в простой двухмерный массив. Затем обработать массив последовательно от первого до последнего уровня и записать поля sortorder и level в таблицу.

Для формирования sortorder не нужно рекурсии (хотя можно, и, вероятно, она работать будет даже быстрее). Достаточно пройтись по массиву одним и тем же циклом. В нём, если рубрика не обработана, для неё формируется поле sortorder из поля sort и родительского sortorder. Если родительская рубрика ещё не обработана, поднимается флаг $unprocessed_rows_exist и цикл запускается ещё раз.

Код

mysql_query("LOCK TABLES dir WRITE");

$result = mysql_query("SELECT id, IFNULL(parent,0) as parent \
                           FROM dir ORDER BY sites DESC, title");

while ($row = mysql_fetch_array($result)) {
    $count++;
    $data["parent"][$row["id"]] = $row["parent"];
    $data["sort"][$row["id"]] = $count;
}

reset($data);

$unprocessed_rows_exist = true;
while($unprocessed_rows_exist) {
    $unprocessed_rows_exist = false;
    while (list($i, $v) = each($data["parent"])) {
        if(($data["parent"][$i] == 0 || !isset($data["sort"][$data["parent"][$i]])) &&
          !isset($data["sortorder"][$i])) {
            $data["sortorder"][$i] = str_pad($data["sort"][$i], $max,
            "0", STR_PAD_LEFT);
            $data["level"][$i] = 0;
        }
        elseif(!isset($data["sortorder"][$i]) &&
                isset($data["sortorder"][$data["parent"][$i]])) {
            $data["sortorder"][$i] = $data["sortorder"][$data["parent"][$i]].
              str_pad($data["sort"][$i], $max, "0", STR_PAD_LEFT);
            $data["level"][$i] = $data["level"][$data["parent"][$i]] + 1;
        }
        elseif(!isset($data["sortorder"][$i]) &&
            isset($data["sort"][$data["parent"][$i]])) {
            $unprocessed_rows_exist = true;
        }
    }

reset($data);


Отмечу, что данный алгоритм не зацикливается при наличии строк с битым полем parent и не пропускает их, а делает корневыми. Рекурсивный алгоритм их просто пропустит.

После выполнения этого цикла мы имеем массивы "id => level" и "id => sortorder". Отправляем в базу всего один запрос, пользуясь внутренними функциями MySQL ELT и FIND_IN_SET:

Код

mysql_query("UPDATE dir SET sortorder=ELT(FIND_IN_SET(id,'".
  implode(",", array_keys($data["sortorder"])).
  "'),". implode(",", $data["sortorder"]).
  "), level=ELT(FIND_IN_SET(id,'".
  implode(",", array_keys($data["level"])).
  "'),". implode(",", $data["level"]).
  ") WHERE id IN (".
  implode(",", array_keys($data["sortorder"])).
  ")");


Конечно же, в отличие от дерева рубрик каталога, в большом форуме много сообщений. Передёргивать их всех при добавлении одного нового нет смысла.

В форумах чаще всего используется сортировка по дате написания сообщения. Поэтому поле sortorder можно смело делать из своего и родительского timestamp'а, выровненного функцией str_pad
PM WWW ICQ Skype GTalk Jabber   Вверх
dm9
Дата 25.4.2007, 23:59 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Дмитрий Копытин
****


Профиль
Группа: Vingrad developer
Сообщений: 3876
Регистрация: 22.7.2002
Где: Москва

Репутация: нет
Всего: 137



Я бы сделал следующие комментарии к коду.

1.
Цитата(DezmASter @  22.4.2007,  16:10 Найти цитируемый пост)
 level (TINYINT(4). 127 уровней - хватит?) и sortorder (VARCHAR(128))

У тебя используется сейчас 127 уровней - значит, под них нужно 2*127=254 байта для строки, в которую ты будешь писать цифры. А не 128.

2. Ты ограничил при этом уровень вложенности 99-ю, так как именно столько может храниться в двухцифровом числе. А делаешь ли ты эти проверки в коде? (Читать код сейчас не хочу, если делаешь, извини. Но всё равно - зачем ограничение 99? Можно, конечно, 16-ричное представление использовать...)

3. sortorder (VARCHAR(128)) я обычно заменяю на sortorder (INTEGER). Так индексировать приходится меньше данных, и меньше искусственности с методе. Обновление же данных очень просто: находим новую позицию для сущности (pos_new), и затем делаем UPDATE dir SET sortorder = sortorder + 1 WHERE sortorder > pos_new;

PM MAIL ICQ   Вверх
SelenIT
Дата 18.12.2007, 08:38 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


баг форума
****


Профиль
Группа: Завсегдатай
Сообщений: 3996
Регистрация: 17.10.2006
Где: Pale Blue Dot

Репутация: 9
Всего: 401



Цитата(dm9 @  25.4.2007,  23:59 Найти цитируемый пост)
sortorder (INTEGER)

Экономия места и выигрыш в скорости индексации бесспорны. Но ведь и максимальный level будет не 127, а от силы 10 (при unsigned bigint и том же ограничении в 99 потомков на уровне), разве нет?


--------------------
Осторожно! Данный юзер и его посты содержат ДГМО! Противопоказано лицам с предрасположенностью к зонеризму!
PM MAIL   Вверх
dm9
Дата 19.12.2007, 00:21 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Дмитрий Копытин
****


Профиль
Группа: Vingrad developer
Сообщений: 3876
Регистрация: 22.7.2002
Где: Москва

Репутация: нет
Всего: 137



Какая-то старая тема очень... smile

Нет, не десять. Потому что одному элементу дерева здесь соответствует увеличение sortorder на единицу.

-- 1
---- 2
-- 3
---- 4
------5
-- 6
-- 7

И так далее.

Естественно, это не очень оптимально при громадных деревьях.
Но в реалиях я обычно применяю этот способ для построения дерева комментариев: а комментов редко бывает больше сотни (для одной комментируемой сущности). При этом в той же таблице комментариев есть поле something_id (FK на комментируемую сущность). И сортировка в этом случае -- своя для каждой сущности. То есть запрос обновления будет выглядеть не как UPDATE dir SET sortorder = sortorder + 1 WHERE sortorder > pos_new; (что я написал выше), а UPDATE dir SET sortorder = sortorder + 1 WHERE sortorder > pos_new AND something_id = 123;
PM MAIL ICQ   Вверх
SelenIT
Дата 19.12.2007, 10:34 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


баг форума
****


Профиль
Группа: Завсегдатай
Сообщений: 3996
Регистрация: 17.10.2006
Где: Pale Blue Dot

Репутация: 9
Всего: 401



dm9,
Цитата(dm9 @  19.12.2007,  00:21 Найти цитируемый пост)
Какая-то старая тема очень...

Вроде не очень - всего полгода. Просто на этой неделе некоторый бум вопросов про деревья...

Цитата(dm9 @  19.12.2007,  00:21 Найти цитируемый пост)
Нет, не десять. Потому что одному элементу дерева здесь соответствует увеличение sortorder на единицу.

Хм... я имел в виду пример из статьи, где в поле с этим названием по сути собирался материализованный путь (по 2 разряда на уровень, оттого и ограничение в 99), и исходил из максимум 20-значного целого в MySQL. Хотя теперь до меня дошло, что если сделать его числовым, то сортировка по нему (как в статье) работать просто не будет. По-моему, Вы говорите о каком-то принципиально другом методе ;).


--------------------
Осторожно! Данный юзер и его посты содержат ДГМО! Противопоказано лицам с предрасположенностью к зонеризму!
PM MAIL   Вверх
dm9
Дата 19.12.2007, 13:15 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Дмитрий Копытин
****


Профиль
Группа: Vingrad developer
Сообщений: 3876
Регистрация: 22.7.2002
Где: Москва

Репутация: нет
Всего: 137



Ну да. Я же, вроде, описал свой подход. И даже привёл примеры.
Посмотрите внимательнее ещё раз. Если непонятно -- могу попробовать поподробнее описать.
PM MAIL ICQ   Вверх
SelenIT
Дата 19.12.2007, 13:23 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


баг форума
****


Профиль
Группа: Завсегдатай
Сообщений: 3996
Регистрация: 17.10.2006
Где: Pale Blue Dot

Репутация: 9
Всего: 401



dm9, т.е. положение элемента в дереве описывают 2 числа - номер строки при выводе и уровень вложенности ("отступ слева"), теперь я правильно понял?


--------------------
Осторожно! Данный юзер и его посты содержат ДГМО! Противопоказано лицам с предрасположенностью к зонеризму!
PM MAIL   Вверх
dm9
Дата 19.12.2007, 14:25 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Дмитрий Копытин
****


Профиль
Группа: Vingrad developer
Сообщений: 3876
Регистрация: 22.7.2002
Где: Москва

Репутация: нет
Всего: 137



И да, и нет.
Эти два числа, по сути, кэш для более быстрых выборок.
Оба они могут быть восстановлены на основе FK parent_id.

Добавлено через 10 минут и 3 секунды
Этот метод хорош для неизменяемой структуры дерева -- например, тот же список комментариев.
Если же структура дерева предполагает изменяться (например, дерево разделов сайта), то нужны другие методы -- этот слишком неоптимален при обновлении, есть у него и другие минусы. Для дерева разделов я использую parent_id, но не сохраняю level и order -- но кэширую результат рекурсивной выборки в PHP-структуру и/или XML.
PM MAIL ICQ   Вверх
SelenIT
Дата 19.12.2007, 15:08 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


баг форума
****


Профиль
Группа: Завсегдатай
Сообщений: 3996
Регистрация: 17.10.2006
Где: Pale Blue Dot

Репутация: 9
Всего: 401



dm9, спасибо за объяснение!


--------------------
Осторожно! Данный юзер и его посты содержат ДГМО! Противопоказано лицам с предрасположенностью к зонеризму!
PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | PHP: Базы Данных | Следующая тема »


 




[ Время генерации скрипта: 0.0821 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.