Столкнулся с совершенно банальной задачей: генерация дерева категорий. Не особенно задумываясь тут же ее решил "что бы работало" при помощи рекурсивной функции, т.к. проект под нагрузкой работать не будет никогда. Но поскольку каталог потом планирую использовать в других проектах, хочется сделать нормально. Итак. Дальше много буков неосмысленного потока моего сознания, так что если куда-то торопитесь смело закрывайте Код скорее всего не рабочий, накидал в Notepad++ по быстрому для примера, ибо открывать NetBeans черевато. Решение а-бы как: [sql] CREATE TABLE categories( `id` INT NOT NULL AUTO_INCREMENT, `pid` INT NOT NULL DEFAULT 0, `childs` INT NOT NULL DEFAULT 0, ............................... PRIMARY KEY(`id`) ); [/sql] функция генерации: PHP: <?php // Да вот такой вот быдлокод. Не писАлось мне в пятницу public function getBranch($pid = 0, $id = 0){ if($id){ $tree = $this->db->query('SELECT `id`, `have_child`, ....., `pid` FROM `categories` WHERE `id`=?i' , array($id), 'rowassoc'); if($tree['childs'] != 0){ $tree['childs'] = $this->getBranch($tree['id']); } return $tree; } else{ $childs = $this->db->query('SELECT `id`, `have_child`, ....., `pid` FROM `categories` WHERE `pid`=?i' , array($pid), 'assoc'); foreach($childs as $key=>$child){ if($child['childs'] != 0){ $childs[$key]['childs'] = $this->getBranch($child['id']); } } return $childs; } } ?> Ну очевидно, что если категорий много и у каждой несколько деток, то будет много запросов к базе, что не комильфо. В голову пришли несколько вариантов оптимизации: 1. а)Вытаскиваем один раз полностью все дерево категорий. б)Запихиваем его в memcached. в)Когда нужно достаем его из memcached == не дергаем базу. г) .............. д) ПРОФИТ! Плохо то, что будет передавать все подкатегории на клиента == трафик 2. Передаем только "корневые" категории и первый уровень вложения. Остальное по запросу подгружаем. Далеко не факт, что пользователю хватит первого уровня вложения == сервер будет дергаться постоянно почем зря. 3. Разумеется переделываем функцию так, чтобы было WHERE `pid` IN (все_Id_подкатегорий_у_которых_дети_есть) Если объединить это все то получается что-то вроде: Дерево категорий живет в Memcached, перегенирим дерево обновленной функцией при изменении данных о категориях. Что бы не генерить лишний трафик выдаем только категорию и первый уровень вложения остальное таки подгружаем Ajax'om. Меня смущает один момент: Отдаем все дерево == генерим лишний трафик. Отдаем часть == нет гарантии что пользователю этого хватит, вполне возможно что лишние AJAX запросы буду куда как печальнее нагрузки на полосу пропускания. Существует ли математическое решение данной проблемы, или же нужно просто под конкретный проект по разному делать, мерять как лучше будет? Посмотрел сайты всяких серьезных дядек такие как: http://www.ozon.ru/ http://www.amazon.com/ http://ebay.com/ http://zakazi.spb.ru/ http://market.yandex.ru/ У них у всех вначале выдается немножко категорий, потом конкретная ветка, когда пользователь выберет. Разве что на ебэе в листинге очень солидная выдача. Но это все похоже продиктовано скорее не оптимизацией а юзабилити. Собственно еще раз вопросы: 1. Отдаем все дерево == генерим лишний трафик. Отдаем часть == нет гарантии что пользователю этого хватит, вполне возможно что лишние AJAX запросы буду куда как печальнее нагрузки на полосу пропускания. Существует ли математическое решение данной проблемы, или же нужно просто под конкретный проект по разному делать, мерять как лучше будет? 2. У Вас проект под нагрузкой? Как Вы сделали? А почему? P.S. Я не страдаю в выходные фигней. Я просто познаю мир.
А нельзя ли дерево хранить в виде структуры в базе? При его изменении, обновлять запись... Тот же мемкешед, только для не страшен для виртуальных хостингов. Или вообще готовый php-массив хранить в папке, а потом его инклудить. http://www.photosight.ru/photos/3101546/
хотим быстро работат с деревьями - читаем про NestedSets http://yandex.ru/yandsearch?rpt=rad&text=NestedSets
И кому верить? Не не не доступ к memcached куда как быстрее чем к БД. Ну если юзать для проекта под нагрузкой то будет как-то так: В момент когда я изменяю файл (админ добавил в водку подкатегрию на родниковой воде) несколько человек его читают и получат в ответ кашу. Это конечно решаемая проблема, но лениво возится.
как часто в дерево будут вноситься изменения? доступ к shared memory куда быстрее чем доступ к memcached
сорри, я тему не читал, но влезу. Нафигачил вчера такой класс: PHP: <?php class Array2Tree { private $array; private $tree; function __construct($array) { $this->tree[0] = array('id'=>0, 'title'=>'root'); $this->array = $array; } private function addEl($el, &$node) { if(!is_array($node)) return; if(array_key_exists($el['pid'], $node)) { $node[$el['pid']]['child'][$el['id']] = array( 'id'=>$el['id'], 'title'=>$el['title'] ); } else { foreach($node as &$subNode) { $this->addEl($el, $subNode['child']); } } } public function convert() { foreach($this->array as $node) $this->addEl($node, $this->tree); return $this->tree; } private function resetKeys($node) { $i = 0; foreach($node as $el) { $newNode[$i] = $el; if(is_array($el['child'])) $newNode[$i]['child'] = $this->resetKeys($el['child']); $i++; } return $newNode; } public function convertAndResetKeys() { $this->convert(); return $this->resetKeys($this->tree); } } ?> суть его работы в следующем: есть таблица `categories` (`id`, `pid`, `title`). ... SELECT `id`, `pid`, `title` FROM `categories` ORDER BY `id`; ... $array = $db->fetchAll // будет массив ассоциативных массивов $a2t = new Array2Tree($array); $tree1 = $a2t->convert(); $tree2 = $a2t->convertAndResetKeys(); теперь $tree1 = массив в виде: Код (Text): 0=>array( 'id'=>0, 'title'=>root, 'child'=>array( 3=>array( 'id'=>3, 'title'=>холодильники, 'сhild'=>array( 5=>array( 'id'=>5, 'title'=>с 2 дверями ) ) ) ) ) $tree2 - Тоже самое, что и $tree1, только ключи с 0 начинаются (вместо 3 и 5 - 0) сорри, что перебил как выбирать отдельные ветви - пока не придумал. Не хотел заморачиваться с дополнительными полями типа hasChild, соседиСлева, соседиСправа. Только id, pid, прочие поля типа title
Kreker, если сайт живет на вирт. хостинге значит нагрузки на него особой нет. Т.к. когда на сайт падает солидная нагрузка он переезжает на свой серв где будет все что душе только угодно)
Самый медленный где и по сравнению с чем? Запись и, особенно, нормализация дерева - всегда тяжелые операции. И можно с ними бороться, улучшать их производительность, только зачем? В обычной веб-практике редко бывает больше тысячи категорий. И добавление категории происходит относительно редко. Относительно выборок.
вот в двух проектах сделал классически id,parent_id и выборки соответстующие и не мучаюсь. а дерево порядка 700 записей
По сравнению с материализованым путем и списками смежности. Списки смежности кстати из «чистых вариантов» самый быстрый в среднем. У него выбор ветви медленный, но не смертельно.
есть такой былинный код PHP: <?php private function getRows() { $res = $this->db->query('SELECT ?c@ FROM ?t ORDER BY ?c ?k', array('id', 'pid', 'title'), 'category', 'id', 'ASC'); while($row = $this->db->fetch($res)) $rows[$row['pid']][$row['id']] = $row['title']; $this->rows = $rows; } function buildTree($tree, $pid = 0) { $tpl = new phparser('templates/admin', 'cache'); $tpl->load('modules/' . $this->module . '/categories', '.xml'); $i = 0; foreach($tree as $id=>$root) { if($pid != $id) continue; if(count($root)) { foreach($root as $key=>$title) { $rows[$i]['id'] = $key; $rows[$i]['title'] = $title; $rows[$i]['child'] = NULL; if(count($tree[$key])) $rows[$i]['child'] = $this->buildTree($tree, $key); $i++; } } } $tpl->l('rows', $rows); return $tpl->get_parse(); } private function writeXML() { $tpl = new phparser('templates/admin', 'cache'); $tpl->load('modules/' . $this->module . '/list', '.xml'); $tpl->v('header', XML_HEADER); $tpl->v('categories', $this->buildTree($this->rows)); file_put_contents('cache/categories.xml', $tpl->get_parse()); } в базе id,pid,title. Прошу обратить внимание именно на 6 строку. Это какой-то алгоритм вывода дерева, подразумевающий формирования массива $a['pid']['id'] = $title; Тогда это дерево безболезненно выводится. Но возникают проблемы: как производить сортировку вывода? К примеру я хочу чтобы в меню "лопаты" находились под "электролобзиками". Тут же будет просто вывод непонятно как отсортированный
тут как ни сортируй, все равно будет сформирован массив a[pid][id], который потом выводится и вся сортировка идет лесом ((. Если делать рекурсивные запросы с сортировкой и тут же их выводить то все будет ок, но все дерево можно забрать одним запросом, так что этого делать не стоит. upd: я вру, все нормально сортирует, если выводить полностью дерево. Возможно будут проблемы при выводе путя от текущей категории до корня с показом подразделов.