За последние 24 часа нас посетили 54973 программиста и 1748 роботов. Сейчас ищут 870 программистов ...

Дерево категорий.

Тема в разделе "Прочее", создана пользователем akrinel, 1 мар 2009.

  1. akrinel

    akrinel Активный пользователь

    С нами с:
    26 янв 2009
    Сообщения:
    955
    Симпатии:
    1
    Адрес:
    Spb
    Столкнулся с совершенно банальной задачей: генерация дерева категорий.

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

    Но поскольку каталог потом планирую использовать в других проектах, хочется сделать нормально.

    Итак. Дальше много буков неосмысленного потока моего сознания, так что если куда-то торопитесь смело закрывайте
    Код скорее всего не рабочий, накидал в 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:
    1.  
    2.      <?php
    3. // Да вот такой вот быдлокод. Не писАлось мне в пятницу
    4. public  function  getBranch($pid = 0, $id = 0){
    5.     if($id){
    6.         $tree  =  $this->db->query('SELECT `id`, `have_child`, ....., `pid`
    7.                                     FROM `categories`
    8.                                     WHERE  `id`=?i'
    9.                                     , array($id), 'rowassoc');
    10.        
    11.         if($tree['childs'] != 0){
    12.             $tree['childs']  =  $this->getBranch($tree['id']);
    13.         }
    14.        
    15.         return  $tree;
    16.     }
    17.     else{
    18.         $childs  =  $this->db->query('SELECT `id`, `have_child`, ....., `pid`
    19.                                       FROM `categories`
    20.                                       WHERE `pid`=?i'
    21.                                       , array($pid), 'assoc');
    22.        
    23.         foreach($childs  as  $key=>$child){
    24.             if($child['childs']  != 0){
    25.                 $childs[$key]['childs']  =  $this->getBranch($child['id']);
    26.             }
    27.         }
    28.  
    29.         return  $childs;
    30.     }
    31. }
    32. ?>
    33.  
    Ну очевидно, что если категорий много и у каждой несколько деток, то будет много запросов к базе, что не комильфо.

    В голову пришли несколько вариантов оптимизации:

    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. Я не страдаю в выходные фигней. Я просто познаю мир.
     
  2. Kreker

    Kreker Старожил

    С нами с:
    8 апр 2007
    Сообщения:
    5.433
    Симпатии:
    0
    А нельзя ли дерево хранить в виде структуры в базе? При его изменении, обновлять запись...
    Тот же мемкешед, только для не страшен для виртуальных хостингов.
    Или вообще готовый php-массив хранить в папке, а потом его инклудить.

    http://www.photosight.ru/photos/3101546/
     
  3. 440Hz

    440Hz Старожил
    Команда форума Модератор

    С нами с:
    21 дек 2012
    Сообщения:
    8.003
    Симпатии:
    1
    Адрес:
    Оттуда
  4. NestedSets - самый медленный способ работы с деревьями
     
  5. akrinel

    akrinel Активный пользователь

    С нами с:
    26 янв 2009
    Сообщения:
    955
    Симпатии:
    1
    Адрес:
    Spb
    И кому верить? :)

    Не не не доступ к memcached куда как быстрее чем к БД.

    Ну если юзать для проекта под нагрузкой то будет как-то так:

    В момент когда я изменяю файл (админ добавил в водку подкатегрию на родниковой воде) несколько человек его читают и получат в ответ кашу. Это конечно решаемая проблема, но лениво возится.
     
  6. Sergey89

    Sergey89 Активный пользователь

    С нами с:
    4 янв 2007
    Сообщения:
    4.796
    Симпатии:
    0
    как часто в дерево будут вноситься изменения?

    доступ к shared memory куда быстрее чем доступ к memcached
     
  7. Koc

    Koc Активный пользователь

    С нами с:
    3 мар 2008
    Сообщения:
    2.253
    Симпатии:
    0
    Адрес:
    \Ukraine\Dnepropetrovsk
    сорри, я тему не читал, но влезу. Нафигачил вчера такой класс:

    PHP:
    1. <?php
    2. class Array2Tree
    3. {
    4.     private $array;
    5.     private $tree;
    6.    
    7.     function __construct($array)
    8.     {
    9.         $this->tree[0] = array('id'=>0, 'title'=>'root');
    10.         $this->array = $array;
    11.     }
    12.    
    13.     private function addEl($el, &$node)
    14.     {
    15.         if(!is_array($node))
    16.             return;
    17.         if(array_key_exists($el['pid'], $node)) {
    18.             $node[$el['pid']]['child'][$el['id']] = array(
    19.                 'id'=>$el['id'],
    20.                 'title'=>$el['title']
    21.             );
    22.         } else {
    23.             foreach($node as &$subNode) {
    24.                 $this->addEl($el, $subNode['child']);
    25.             }
    26.         }
    27.     }
    28.    
    29.     public function convert()
    30.     {
    31.         foreach($this->array as $node)
    32.             $this->addEl($node, $this->tree);
    33.        
    34.         return $this->tree;
    35.     }
    36.    
    37.     private function resetKeys($node)
    38.     {
    39.         $i = 0;
    40.         foreach($node as $el) {
    41.             $newNode[$i] = $el;
    42.             if(is_array($el['child']))
    43.                 $newNode[$i]['child'] = $this->resetKeys($el['child']);
    44.             $i++;
    45.         }
    46.         return $newNode;
    47.     }
    48.    
    49.     public function convertAndResetKeys()
    50.     {
    51.         $this->convert();
    52.         return $this->resetKeys($this->tree);
    53.     }
    54. }
    55. ?>
    суть его работы в следующем: есть таблица `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):
    1. 0=>array(
    2.     'id'=>0,
    3.     'title'=>root,
    4.     'child'=>array(
    5.         3=>array(
    6.             'id'=>3,
    7.             'title'=>холодильники,
    8.             'сhild'=>array(
    9.                 5=>array(
    10.                 'id'=>5,
    11.                 'title'=>с 2 дверями
    12.                 )
    13.             )
    14.         )
    15.     )
    16. )
    $tree2 - Тоже самое, что и $tree1, только ключи с 0 начинаются (вместо 3 и 5 - 0)

    сорри, что перебил

    как выбирать отдельные ветви - пока не придумал. Не хотел заморачиваться с дополнительными полями типа hasChild, соседиСлева, соседиСправа. Только id, pid, прочие поля типа title
     
  8. Kreker

    Kreker Старожил

    С нами с:
    8 апр 2007
    Сообщения:
    5.433
    Симпатии:
    0
    На виртуальных хостингах разве есть memcached?
     
  9. akrinel

    akrinel Активный пользователь

    С нами с:
    26 янв 2009
    Сообщения:
    955
    Симпатии:
    1
    Адрес:
    Spb
    Kreker, если сайт живет на вирт. хостинге значит нагрузки на него особой нет. Т.к. когда на сайт падает солидная нагрузка он переезжает на свой серв где будет все что душе только угодно)
     
  10. AlexGousev

    AlexGousev Активный пользователь

    С нами с:
    25 мар 2006
    Сообщения:
    1.505
    Симпатии:
    0
    Адрес:
    Москва
    Самый медленный где и по сравнению с чем? Запись и, особенно, нормализация дерева - всегда тяжелые операции. И можно с ними бороться, улучшать их производительность, только зачем? В обычной веб-практике редко бывает больше тысячи категорий. И добавление категории происходит относительно редко. Относительно выборок.
     
  11. Kreker

    Kreker Старожил

    С нами с:
    8 апр 2007
    Сообщения:
    5.433
    Симпатии:
    0
    http://habrahabr.ru/blogs/development/47280/ не совсем "самый"
     
  12. AlexGousev

    AlexGousev Активный пользователь

    С нами с:
    25 мар 2006
    Сообщения:
    1.505
    Симпатии:
    0
    Адрес:
    Москва
    Kreker
    Я одним глазом пролистал и повторю ту же мысль:
     
  13. 440Hz

    440Hz Старожил
    Команда форума Модератор

    С нами с:
    21 дек 2012
    Сообщения:
    8.003
    Симпатии:
    1
    Адрес:
    Оттуда
    вот в двух проектах сделал классически id,parent_id и выборки соответстующие и не мучаюсь. а дерево порядка 700 записей
     
  14. AlexGousev

    AlexGousev Активный пользователь

    С нами с:
    25 мар 2006
    Сообщения:
    1.505
    Симпатии:
    0
    Адрес:
    Москва
    Глубина?
     
  15. 440Hz

    440Hz Старожил
    Команда форума Модератор

    С нами с:
    21 дек 2012
    Сообщения:
    8.003
    Симпатии:
    1
    Адрес:
    Оттуда
    3-5
     
  16. По сравнению с материализованым путем и списками смежности. Списки смежности кстати из «чистых вариантов» самый быстрый в среднем. У него выбор ветви медленный, но не смертельно.
     
  17. Mr.M.I.T.

    Mr.M.I.T. Старожил

    С нами с:
    28 янв 2008
    Сообщения:
    4.586
    Симпатии:
    1
    Адрес:
    у тебя канфетка?
    Да в пхп файле структуру хранить и всё =)
     
  18. Koc

    Koc Активный пользователь

    С нами с:
    3 мар 2008
    Сообщения:
    2.253
    Симпатии:
    0
    Адрес:
    \Ukraine\Dnepropetrovsk
    есть такой былинный код
    PHP:
    1. <?php
    2.     private function getRows()
    3.     {
    4.         $res = $this->db->query('SELECT ?c@ FROM ?t ORDER BY ?c ?k', array('id', 'pid', 'title'), 'category', 'id', 'ASC');
    5.         while($row = $this->db->fetch($res))
    6.             $rows[$row['pid']][$row['id']] = $row['title'];
    7.        
    8.         $this->rows = $rows;
    9.     }
    10.    
    11.     function buildTree($tree, $pid = 0)
    12.     {
    13.         $tpl = new phparser('templates/admin', 'cache');
    14.         $tpl->load('modules/' . $this->module . '/categories', '.xml');
    15.         $i = 0;
    16.        
    17.         foreach($tree as $id=>$root) {
    18.             if($pid != $id) continue;
    19.             if(count($root)) {
    20.                 foreach($root as $key=>$title) {
    21.                     $rows[$i]['id'] = $key;
    22.                     $rows[$i]['title'] = $title;
    23.                     $rows[$i]['child'] = NULL;
    24.                     if(count($tree[$key]))
    25.                         $rows[$i]['child'] = $this->buildTree($tree, $key);
    26.                     $i++;
    27.                 }
    28.             }
    29.         }
    30.        
    31.         $tpl->l('rows', $rows);
    32.         return $tpl->get_parse();
    33.     }
    34.    
    35.     private function writeXML()
    36.     {
    37.         $tpl = new phparser('templates/admin', 'cache');
    38.         $tpl->load('modules/' . $this->module . '/list', '.xml');
    39.        
    40.         $tpl->v('header', XML_HEADER);
    41.         $tpl->v('categories', $this->buildTree($this->rows));
    42.         file_put_contents('cache/categories.xml', $tpl->get_parse());
    43.     }
    44.  
    в базе id,pid,title.

    Прошу обратить внимание именно на 6 строку. Это какой-то алгоритм вывода дерева, подразумевающий формирования массива $a['pid']['id'] = $title; Тогда это дерево безболезненно выводится. Но возникают проблемы: как производить сортировку вывода? К примеру я хочу чтобы в меню "лопаты" находились под "электролобзиками". Тут же будет просто вывод непонятно как отсортированный
     
  19. Psih

    Psih Активный пользователь
    Команда форума Модератор

    С нами с:
    28 дек 2006
    Сообщения:
    2.678
    Симпатии:
    6
    Адрес:
    Рига, Латвия
    Сортируйте в базе по нескольким критериям и в итоге массивы получатся как надо.
     
  20. Koc

    Koc Активный пользователь

    С нами с:
    3 мар 2008
    Сообщения:
    2.253
    Симпатии:
    0
    Адрес:
    \Ukraine\Dnepropetrovsk
    тут как ни сортируй, все равно будет сформирован массив a[pid][id], который потом выводится и вся сортировка идет лесом ((.

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

    upd: я вру, все нормально сортирует, если выводить полностью дерево. Возможно будут проблемы при выводе путя от текущей категории до корня с показом подразделов.