The task of displaying trees in MySql. Display Method on Stored Procedures
I really wanted to raise the issue of tree structures in MySql. Specifically, about fetching and storing data ...
Oracle and PostgresSQL users have a good life, these databases have built-in tools for fetching recursive data (see Hierarchical (recursive) queries ).
Mysql users have to work with what is, that is, work on the client side.
Since this topic has not been raised once, I’ll try to talk about a specific problem and how to solve it.
Task
There is a project in php, it has a twofold structure implemented in the mysql catalog table (id int NOT NULL, pid int NOT NULL, ...)
pid is the id of the parent. That is, the data is stored in one table, each cell refers to the parent. In this way the whole tree is formed.
Naturally, a bunch of related data is stored in the table, the description of which I will omit, since they will not affect the essence of the task, but will only get in the way.
An example of such data storage:
- CREATE TABLE catalog(
- id INT NOT NULL PRIMARY,
- pid INT NOT NULL,
- someData text NOT NULL)
- comment = "табличка с рекурентной стркутурой";
* This source code was highlighted with Source Code Highlighter.| id | pid | someData |
|---|---|---|
| 1 | 0 | Catalog |
| 2 | 1 | Book 1 |
| 3 | 1 | Book 2 |
| 4 | 3 | Chapter 1 |
| 5 | 3 | Chapter 2 |
| 6 | 3 | Chapter 3 |

The entire tree is sampled recursively on the client side. The total base size is 500Mb, and will only continue to grow. Since there are many branches and nodes, the total number of queries to the database currently ranges from 300 to 1000.
As a result of recursive sampling, an array of data is formed (for some reason, also of a recursive form), which is given to the template engine.
The task is to optimize the selection so as to pull out the necessary data with one request (well, or two or three, but not so that there are so many ...)
Solution option
The task as a whole is trivial. She was repeatedly decided. Additional data is entered that is used to limit the selection from the table. The whole question comes down to what kind of additional field (s) it is. One way or another, additional data leads to redundancy, which means overhead for maintaining data correctness.
In publications, the most common solution is an additional field of the varchar type, in which id-tags are stored with text separated by a special character. This field is sorted and limited to something like like% ...%.
In general, the solution is very good, but has its drawbacks:
- natural restriction on hierarchy nesting.
- Overhead and difficulty maintaining this field. (in my case, if you write a script that supports this field, it will just hang by timeout, which means you need to do these actions in cron)
- a serious load on the network, which is the basis for the statement of the problem, the load didn’t go away, it’s just in the background, and the server slows down anyway.
- in case of transfer of tree nodes, you need to update the data of this field.
As a result, we get a rather difficult and difficult decision. There was no time for its implementation, as always.
To solve all the above problems, you need to act differently. Firstly, the formation of additional data is best done where this data is located, that is, on the side of the database. Secondly, supporting additional data is an overhead operation, so it’s better to update it every time.
This concept is implemented as follows: a simple procedure is written that would generate data for the tmp__index temporary plate, making a recursive tree traversal.
Here is the actual procedure code: Starting the procedure is preceded by creating a temporary plate, and then a simple select tears out the necessary data.
- DELIMITER $$
- DROP PROCEDURE IF EXISTS `getIndexToTmpTable`$$
- /**
- main_id - идентификатор корня или метка поиска
- search_id - идентификатор для начала поиска
- zlevel - максимальный уровень вложенности, чтобы не упрыгать слишком глубоко,
- - установить -1 - чтобы отключить ограничение по вложенности
- sublevel - текущий уровень вложенности, для того чтобы складировать в базу
- - установить 1
- */
- CREATE PROCEDURE `getIndexToTmpTable` (
- in main_id INT,
- in search_id INT,
- in zlevel INT,
- in sublevel INT )
- BEGIN
- DECLARE done INT DEFAULT 0;
- DECLARE catalog_id INT;
- DECLARE catalog_pid INT;
- DECLARE cur1 CURSOR FOR select id,pid from catalog where pid=search_id;
- DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
- IF sublevel<=1 THEN /** чтобы установть только раз*/
- IF zlevel<=0 THEN
- /** число наобум */
- SET max_sp_recursion_depth= 15;
- ELSE
- SET max_sp_recursion_depth= zlevel+1;
- END IF;
- END IF;
- OPEN cur1;
- IF main_id = search_id THEN
- /** вставим саму запись, чтобы был полный боекомплект */
- insert into tmp__index set
- id = main_id,
- pid =(select pid from catalog where id=main_id limit 1),
- rid =main_id,
- level = sublevel-1;
- END IF;
- /** нужно влючить глубину рекурсии */
- REPEAT
- FETCH cur1 INTO catalog_id,catalog_pid;
- IF NOT done THEN
- /** вставим текущую найденную запись */
- insert into tmp__index set
- id = catalog_id,
- pid = catalog_pid,
- rid = main_id,
- level = sublevel;
- /** спустимся по ниже */
- call getIndexToTmpTable(main_id,catalog_id,zlevel,sublevel+1);
- END IF;
- UNTIL done END REPEAT;
- CLOSE cur1;
- END$$
- DELIMITER ;
* This source code was highlighted with Source Code Highlighter.- CREATE TEMPORARY TABLE tmp__index(id int,pid int ,rid int);
- call getIndexToTmpTable(
,,1,1); - select c.* from catalog as c join tmp__index as t on t.rid=c.id
* This source code was highlighted with Source Code Highlighter.Let's stop for a moment and think about what we have done and how good it is:
- we do all 3 queries to the database
- got rid of the problems associated with supporting an additional field, since everything is dynamically generated
- this procedure cannot loop, mysql limits the recursion depth
- we can control how deeply we need to go around the tree (that is, to what level)
- we know everything about every element, right down to the level at which it is
- secondary execution of the procedure on the same data can be eliminated using the local cache of the query result.
Honestly, I was very surprised that such functionality is so easily implemented by database tools. However, we go further, the task is not completely resolved. Now the question arises - what to do with this array. There are only two options:
- rewrite all the templates in a new way.
- make the tree structure linear.
I believe that if possible, it is better to refactor the templates so that they understand the linear data structure. This is the most solid way. But unfortunately in my case it’s easier and faster to make a tree structure, rather than shortening the entire project ...
So I did this: I formed a tree carapace based on links, sequentially placing a link to the child element in the parent element. After the tree frame was formed, I went through it recursively and created a tree with real data.

The figure shows the view of the frame I receive. I schematically indicated the name of the field there, but in reality only identifiers are stored there in the form $ id => array (...), where $ id is the identifier of the element, and the array contains a set of links to the root elements of the array. It is important to understand that records with the same name refer to the same memory location. The total amount of data stored in memory is the number of elements + the overhead of storing an array of links.
Here is the conversion code, it is inside the class. Please do not judge strictly, it was written quickly, from under a stick:
- protected $_idToData = null;
- protected $_dataArray = null;
- /**
- * генерация рекурентно данных на основе дерева ссылок
- * для работы заполнить дерево ссылок, массив связки с данными и массив данных
- * $_idToData, $_data
- *
- * @param array& $curItemFrom
- * @param array& $curItemTo
- */
- protected function _recGenTreeResult(array&$curItemFrom,array&$curItemTo)
- {
- foreach($curItemFrom as $id=>&$list)
- {
- if(!isset($this->_idToData[$id]) || !isset($this->_dataArray[ $this->_idToData[$id] ]))
- throw new Exception('recursion build error!');
- $curItemTo[] = $this->_dataArray[ $this->_idToData[$id] ];
- $i = count($curItemTo)-1;
- if(count($list))
- {
- $curItemTo[$i]['dir'] = array();
- $this->_recGenTreeResult(&$list,&$curItemTo[$i]['dir']);
- }
- }
- }
- function listToTree($dataArray)
- {
- $this->_dataArray = &$dataArray;
- $this->_idToData = array();
- $parentList = array();
- $rootIds = array();
- // а теперь делаем рекурентную структуру для вывода
- // сохраняем id-> num в массиве data для быстрого доступа
- foreach ($this->_dataArray as $k=>$d)
- $this->_idToData[$d['id']] = $k;
- foreach ($this->_dataArray as $k=>$d)
- {
- // делаем соотношения $pid=>array($id, ...), чтобы потом превратить его в дерево
- $parentList[$d['pid']][$d['id']]= array();
- // если есть, то значит у этого товарища есть родитель ...
- if(isset($this->_idToData[$d['pid']]))
- {
- // берем его родителя и делаем в него ссылку на этого товарища
- if(isset($parentList[ $this->_dataArray[ $this->_idToData[$d['pid']] ]['pid']]))
- if(empty($parentList[ $this->_dataArray[ $this->_idToData[$d['pid']] ]['pid']][ $d['pid'] ]))
- $parentList[ $this->_dataArray[ $this->_idToData[$d['pid']] ]['pid']][ $d['pid'] ] = &$parentList[$d['pid']];
- }
- else $rootIds [$d['pid'] ] = true;
- }
- $result = array();
- foreach(array_keys($rootIds) as $pid)
- $this->_recGenTreeResult(&$parentList[$pid],&$result);
- return $result;
- }
* This source code was highlighted with Source Code Highlighter.As a result, we got a quite workable thing. In the case of a large base and depth of recursion, this solution can also be bad. specifically, it can be bad for mySql server. I would solve this problem by introducing a caching table (constant) that would store the results of the descent from the beginning of a certain element and copy them to a temporary table. That is, the classic recursion cache would be obtained, only in SQL. This is close to dynamic programming techniques. Maybe someday I’ll realize it if the local cache is not enough for stable operation. Although I think that's enough ...
links to what to read:
- Php conf2 2008 conference - tree structure slides
- stored procedures, official documentation: http://dev.mysql.com/doc/refman/5.1/en/create-procedure.html
- cursors, official documentation: http://dev.mysql.com/doc/refman/5.1/en/cursors.html
- article about cursors: http://habrahabr.ru/blogs/mysql/46333/
- links in php: http://php.su/learnphp/?re
- Hierarchical (recursive) queries
- Full Hierarchy Database Hierarchies
UPD The article was written by Nikolai Punin ([email protected]), which, unfortunately, has no invite yet.