Обход дерева в mysql

Вывод дерева mysql

Где index — это порядок элемента. Нужно одним запросом сформировать путь, получающийся при обходе в глубину. То есть если у нас такой вид:

1 0 1 2 0 2 3 1 1 4 1 2 5 2 1 6 0 3 

То выведется 1 3 4 2 5 6 Нужно на чистом mysql без использования php и тд. Заранее спасибо P.S. Менять в структуре, к сожалению, ничего нельзя Заранее спасибо

В PostgreSQL поддерживаются рекурсивные запросы WITH RECURSIVE, а в Oracle иерархические запросы (hierarchical query clause (also known as “CONNECT BY query”) or recursive subquery factoring).

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

2 ответа 2

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

select id,path from ( select @path:=if(@id!=id,`index`, coalesce( (select concat(`index`,'/',@path) from Tree1 where ,@path) ) path, @pid:=if(@id!=id,parent, (select parent from Tree1 N where ), @id:=id id, N from Tree1 T, (select @n:=@n+1 N from Tree1,(select @n:=0) N limit 3) Seq, (select @id:=0,@path:='') X order by id, N ) X where N=3 order by path 

Так как ни рекурсии ни циклов в обычном смысле слов нет, то запрос эмулирует рекурсию с помощью эмуляции цикла. В качестве цикла используется склейка с любой таблицей, в которой записей не менее, чем максимально возможный уровень иерархии в дереве. В данном примере используется выборка из той же самой таблицы, явно ограниченная limit 3 (если вложенность дерева больше — число надо соответственно увеличить). Собственно данные из этой таблицы не берутся, а создаются порядковые номера ( N ).

Читайте также:  Потолок под дерево балки

После этой склейки у нас в выборке появляется по 3 записи с одним и тем же id. На каждой из них, с помощью переменных, мы получаем id очередной родительской ветки, углубляясь по дереву (так как переменные помнят значения, сформированные в предыдущей строке). Самое главное, что мы тут формируем это путь к данной записи в виде индексов сортировки (если index может быть более 9 то для правильной сортировки надо выравнивать его в пути до одинаковой длины с помощью lpad() )

В какой то момент записи родителей заканчиваются, мы дошли до корня дерева, тогда подзапрос индекса родителя возвращает NULL и при этом с помощью coalesce мы сохраняем ранее сформированный путь. Таким образом в последней записи нашего «цикла» у нас гарантированно будет полный путь до данного листа дерева. Нам остается только убрать из выборки рабочие строки цикла с частично сформированными путями, т.е. выбрать только последние строки ( where N=3 ) и отсортировать по пути индексов.

Источник

Задача отображения деревьев в MySql. Способ отображения на хранимых процедурах

Очень хотелось поднять вопрос о древовидных структурах в MySql. А конкретно о выборках и хранении данных…
Пользователям Oracle и PostgresSQL живется хорошо, в этих БД есть встроенные средства выборки рекурентных данных (см. Иерархические (рекурсивные) запросы).
Пользователям Mysql приходится работать уже с тем, что есть, то есть работа на стороне клиента.
Поскольку эта тема не однократно поднималась, то я попробую рассказать о конкретной задаче и способе её решения.

Задача

Есть проект на php, в нем двевовидная структура реализованнная в таблице mysql catalog (id int NOT NULL,pid int NOT NULL, . )
pid — это id родителя. То есть данные хранятся в одной таблице, каждая ячейка ссылается на родительскую. Таким образом формируется все дерево.
Естественно в таблице хранится куча связанных данных, описание которых я опущу, так как они на суть задачи не повлияют, а будут только мешаться.

Пример такого хранения данных:

  1. CREATE TABLE catalog (
  2. id INT NOT NULL PRIMARY ,
  3. pid INT NOT NULL ,
  4. someData text NOT NULL )
  5. comment = «табличка с рекурентной стркутурой»;

* This source code was highlighted with Source Code Highlighter .

id pid someData
1 0 Каталог
2 1 Книга 1
3 1 Книга 2
4 3 Глава 1
5 3 Глава 2
6 3 Глава 3

Производится выборка данных всего дерева рекурентно на стороне клиента. Общий объем базы 500Мб, и дальше будет только расти. Поскольку ветвей и узлов много, то общее количество запросов к базе на данный момент колеблется от 300 до 1000.

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

Вариант решения

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

В публикациях наиболее часто встречается решение с дополнительным поле типа varchar, в котором хранятся id-шники текстом, разделенные спецсимволом. По этому полю осуществляется сортировка и ограничение чем-нибудь типа like %. %.

  1. естественное ограничение на вложенность иерархии.
  2. накладные расходы и сложности по поддержке этого поля. (в моем случае если писать скрипт занимающийся поддержкой этого поля, то он будет просто виснуть по таймауту, а значит нужно делать эти действия в cron)
  3. серьезнная нагрузка на сеть, которая и является основой для постановки задачи, нагрузка не куда не делась, просто она в фоне, а сервер все-равно тормозит.
  4. в случае переноса узлов дерева нужно обновлять данные этого поля.

В итоге получаем достаточно тяжелое и сложное решение. Времени на его реализацию как всегда не было.

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

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

  1. DELIMITER $$
  2. DROP PROCEDURE IF EXISTS `getIndexToTmpTable`$$
  3. /**
  4. main_id — идентификатор корня или метка поиска
  5. search_id — идентификатор для начала поиска
  6. zlevel — максимальный уровень вложенности, чтобы не упрыгать слишком глубоко,
  7. — установить -1 — чтобы отключить ограничение по вложенности
  8. sublevel — текущий уровень вложенности, для того чтобы складировать в базу
  9. — установить 1
  10. */
  11. CREATE PROCEDURE `getIndexToTmpTable` (
  12. in main_id INT ,
  13. in search_id INT ,
  14. in zlevel INT ,
  15. in sublevel INT )
  16. BEGIN
  17. DECLARE done INT DEFAULT 0;
  18. DECLARE catalog_id INT ;
  19. DECLARE catalog_pid INT ;
  20. DECLARE cur1 CURSOR FOR select id,pid from catalog where pid=search_id;
  21. DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
  22. IF sublevel
  23. IF zlevel
  24. /** число наобум */
  25. SET max_sp_recursion_depth= 15;
  26. ELSE
  27. SET max_sp_recursion_depth= zlevel+1;
  28. END IF ;
  29. END IF ;
  30. OPEN cur1;
  31. IF main_id = search_id THEN
  32. /** вставим саму запись, чтобы был полный боекомплект */
  33. insert into tmp__index set
  34. pid =( select pid from catalog where limit 1),
  35. rid =main_id,
  36. level = sublevel-1;
  37. END IF ;
  38. /** нужно влючить глубину рекурсии */
  39. REPEAT
  40. FETCH cur1 INTO catalog_id,catalog_pid;
  41. IF NOT done THEN
  42. /** вставим текущую найденную запись */
  43. insert into tmp__index set
  44. pid = catalog_pid,
  45. rid = main_id,
  46. level = sublevel;
  47. /** спустимся по ниже */
  48. call getIndexToTmpTable(main_id,catalog_id,zlevel,sublevel+1);
  49. END IF ;
  50. UNTIL done END REPEAT;
  51. CLOSE cur1;
  52. END $$
  53. DELIMITER ;

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

  1. CREATE TEMPORARY TABLE tmp__index(id int ,pid int ,rid int );
  2. call getIndexToTmpTable(,,1,1);
  3. select c.* from catalog as c join tmp__index as t on t.rid=c.id
  1. делаем все 3-я запросами к БД
  2. избавились от проблем связанных с поддержкой дополнительного поля, так как все генерится динамически
  3. данная процедура не может зациклится, mysql ограничивает на глубину рекурсии
  4. мы можем сами контролировать насколько глубоко нужно обойти дерево (то есть до какого уровня)
  5. мы знаем о каждом элементе все, вплоть до того, на каком уровне он находится
  6. вторичное исполнение процедуры на одних и тех же данных можно устранить, используя локальный кеш результата запроса.
  1. переписать все шаблоны на новый лад.
  2. сделать древовидную структуру из линейной.

Так что я сделал так: сформировал каракас дерева на основе ссылок, последовательно помещая ссылку на элемент-дитя в элемент-родитель. После формирования каркаса дерева я проходил по нему рекурентно и создавал уже дерево с реальными данными.

На рисунке показан вид получаемого мной каркаса. Я схематично указал там название поля, но реально там хранятся только идентификаторы в виде $id=>array(. ), где $id — инедтификатор элемента, а массив содержит набор ссылок на корневые элементы массива. Важно понимать, что записи с одним и тем же именем ссылаются на одно и тоже место памяти. Общий объем хранимых в памяти данных — число элементов + накладные расходы нах хранение массива ссылок.

Вот код преобразования, он в внутри класса. Прошу не судить строго, писалось быстро, из-под палки:

  1. protected $_idToData = null ;
  2. protected $_dataArray = null ;
  3. /**
  4. * генерация рекурентно данных на основе дерева ссылок
  5. * для работы заполнить дерево ссылок, массив связки с данными и массив данных
  6. * $_idToData, $_data
  7. *
  8. * @param array& $curItemFrom
  9. * @param array& $curItemTo
  10. */
  11. protected function _recGenTreeResult(array&$curItemFrom,array&$curItemTo)
  12. foreach ($curItemFrom as $id=>&$list)
  13. if (!isset($ this ->_idToData[$id]) || !isset($ this ->_dataArray[ $ this ->_idToData[$id] ]))
  14. throw new Exception( ‘recursion build error!’ );
  15. $curItemTo[] = $ this ->_dataArray[ $ this ->_idToData[$id] ];
  16. $i = count($curItemTo)-1;
  17. if (count($list))
  18. $curItemTo[$i][ ‘dir’ ] = array();
  19. $ this ->_recGenTreeResult(&$list,&$curItemTo[$i][ ‘dir’ ]);
  20. >
  21. >
  22. >
  23. function listToTree($dataArray)
  24. $ this ->_dataArray = &$dataArray;
  25. $ this ->_idToData = array();
  26. $parentList = array();
  27. $rootIds = array();
  28. // а теперь делаем рекурентную структуру для вывода
  29. // сохраняем id-> num в массиве data для быстрого доступа
  30. foreach ($ this ->_dataArray as $k=>$d)
  31. $ this ->_idToData[$d[ ‘id’ ]] = $k;
  32. foreach ($ this ->_dataArray as $k=>$d)
  33. // делаем соотношения $pid=>array($id, . ), чтобы потом превратить его в дерево
  34. $parentList[$d[ ‘pid’ ]][$d[ ‘id’ ]]= array();
  35. // если есть, то значит у этого товарища есть родитель .
  36. if (isset($ this ->_idToData[$d[ ‘pid’ ]]))
  37. // берем его родителя и делаем в него ссылку на этого товарища
  38. if (isset($parentList[ $ this ->_dataArray[ $ this ->_idToData[$d[ ‘pid’ ]] ][ ‘pid’ ]]))
  39. if (empty($parentList[ $ this ->_dataArray[ $ this ->_idToData[$d[ ‘pid’ ]] ][ ‘pid’ ]][ $d[ ‘pid’ ] ]))
  40. $parentList[ $ this ->_dataArray[ $ this ->_idToData[$d[ ‘pid’ ]] ][ ‘pid’ ]][ $d[ ‘pid’ ] ] = &$parentList[$d[ ‘pid’ ]];
  41. >
  42. else $rootIds [$d[ ‘pid’ ] ] = true ;
  43. >
  44. $result = array();
  45. foreach (array_keys($rootIds) as $pid)
  46. $ this ->_recGenTreeResult(&$parentList[$pid],&$result);
  47. return $result;
  48. >

В итоге, получили вполне работоспособную штуку. В случае большой базы и глубины рекурсии данному решению тоже может быть плохо. конкретно может быть плохо mySql-серверу. я бы решал эту проблему введением кеширующей таблицы (постоянной), которая бы хранила бы результаты спуска от начала определенного элемента, и копировала бы их во временную таблицу. То есть получался бы классический кеш рекурсии, только на SQL. Это близко к методам динамического программирования. Может быть когда-нибудь дореализую, если локального кеша не хватит на стабильную работу. Хотя я думаю, что хватит…

UPD Статья написана Пуниным Николаем (nicolay.punin@gmail.com), у которого, к сожалению, пока нет инвайта.

Источник

Оцените статью