Finding depth in a list model
My production data is similar to the adjacency list model shown here ...
http://mysql.stu.edu.tw/tech-resources/articles/hierarchical-data.html
My questions are - how do I know how deep the nesting is? In this case it is 4 from the last "Flash" sheet to the first "Electronics" node. Is there a query that will return this maximum depth number?
source to share
MYSQL has no CTEs nor recursion functions. You can solve it with a "classic loop":
DELIMITER $$
DROP FUNCTION IF EXISTS figure_up_depth $$
--encapsulated as a function:
CREATE FUNCTION figure_up_depth(idToSearch INT) RETURNS INT BEGIN
--var to count steps from node to root:
DECLARE depth INT;
SET depth = 1;
--while we are not on root node:
WHILE ( idToSearch is not null) DO
SET idToSearch = ( select parent
from category
where category_id = idToSearch
);
SET depth = depth + 1;
END WHILE;
RETURN depth;
END;
$$
DELIMITER ;
Check:
mysql> select figure_up_depth(7);
+--------------------+
| figure_up_depth(7) |
+--------------------+
| 4 |
+--------------------+
1 row in set (0.00 sec)
EDITED July 8, 2015
I understand that the OP is asking for the max depth , not the node depth. Easy:
mysql> select max( figure_up_depth(category_id) ) as max_depth
from category;
If performance matters , the correct solution is:
- Place everything
categories.parent
in a temporary tabletable1
. - Join table1 to categories with
categories.id = table1.id
, place the result in the combined fieldcategories.parent
in a temporary tabletable2
. - Drop
table1
and renametable2
astable1
. - Go to step 2 if it
table1
has strings.
The number of iteration cycles will be the depth of the tree:
DELIMITER $$
DROP PROCEDURE IF EXISTS letsgo;
CREATE PROCEDURE letsgo() BEGIN
DECLARE R int;
DECLARE D int;
SET D=0;
DROP TEMPORARY TABLE IF EXISTS children;
CREATE TEMPORARY TABLE children AS (SELECT category_id as id
FROM category
WHERE parent is NULL );
DROP TEMPORARY TABLE IF EXISTS children_prev;
CREATE TEMPORARY TABLE children_prev AS (SELECT * FROM children );
SET R = ( SELECT count(*) FROM children );
WHILE ( R > 0 ) DO
DROP TEMPORARY TABLE IF EXISTS children_aux;
CREATE TEMPORARY TABLE children_aux AS (
SELECT category_id as id
FROM category R
INNER JOIN children_prev C on C.id = R.parent
);
SET R = ( SELECT count(*) FROM children_aux );
INSERT INTO children
SELECT * FROM children_aux;
TRUNCATE TABLE children_prev;
INSERT INTO children_prev
SELECT * FROM children_aux;
SET D=D+1;
END WHILE;
SELECT D;
END;
$$
DELIMITER ;
Testing:
mysql> call letsgo();
+------+
| D |
+------+
| 4 |
+------+
1 row in set (0.14 sec)
Note: sorry for the dirty solution, this is mysql: no CTE, no recursion, no selection in function recursion, not DO-While, ...
source to share
For a solution that works for the ultimate "maximum" depth, you can use a query like this:
SELECT CASE
WHEN MAX(l8.category_id IS NOT NULL) THEN 8
WHEN MAX(l7.category_id IS NOT NULL) THEN 7
WHEN MAX(l6.category_id IS NOT NULL) THEN 6
WHEN MAX(l5.category_id IS NOT NULL) THEN 5
WHEN MAX(l4.category_id IS NOT NULL) THEN 4
WHEN MAX(l3.category_id IS NOT NULL) THEN 3
WHEN MAX(l2.category_id IS NOT NULL) THEN 2
WHEN MAX(l1.category_id IS NOT NULL) THEN 1
ELSE 0
END AS max_depth
FROM category l1
LEFT JOIN category l2 ON l2.parent = l1.category_id
LEFT JOIN category l3 ON l3.parent = l2.category_id
LEFT JOIN category l4 ON l4.parent = l3.category_id
LEFT JOIN category l5 ON l5.parent = l4.category_id
LEFT JOIN category l6 ON l6.parent = l5.category_id
LEFT JOIN category l7 ON l7.parent = l6.category_id
LEFT JOIN category l8 ON l8.parent = l7.category_id
WHERE l1.parent IS NULL
In this case, if the "tree" contains more than eight levels, the query will return 8, the maximum depth it checks to.
This can be expanded, but this approach requires some ultimate maximum. (MySQL has a limit on the number of table references in a query).
The "root" node of the tree will be NULL for the parent. So the node is at level 1. (There might be multiple rows with NULL, multiple roots. ("I want to select a sibling tree with a maximum depth of 1 and choose to return 0 if there is no" root "node (s).
The expression ln.category_id IS NOT NULL
will return 1 if it is true and 0 if it is false. If there is any line where it is not empty, we know that the tree is at least at that level, that there are many levels.
The CASE expression basically checks: is there a tree at least 8 levels? If so, we're done. If not, then the tree is at least 7 levels? If not, is it at least six levels? and etc.
If you remove that CASE wrapper expression and aggregates MAX
, what you get is every node in the tree, and it goes back to the root node. You can order them from top to bottom ... level 1, level 2, level 3, etc.
To check the maximum depth of the tree, we want to check from the bottom up.
To check for unlimited depth, you need recursion, and MySQL does not support anything like that in the context of a SQL statement. You have entered the scope of a MySQL stored program (procedure or function) to get a recursive solution.
(Other databases give us a convenient syntax CONNECT BY
. But alas, MySQL doesn't.)
source to share
Well that's a good long way :)
begin transaction
CREATE TABLE #category(
category_id INT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL);
INSERT INTO #category
VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
(4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),
(7,'MP3 PLAYERS',6),(8,'FLASH',7),
(9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);
DECLARE @SQL NVARCHAR(MAX)
DECLARE @SQLF NVARCHAR(MAX)
DECLARE @SQLFINAL NVARCHAR(MAX)
DECLARE @CT as VARCHAR(10)
DECLARE @CTP as VARCHAR(10)
DECLARE @SQLFINALP as NVARCHAR(MAX)
SET @CT = 1
SET @CTP = 1
SET @SQL = 'Select t1.name ''lev1'' '
SET @SQLF = 'FROM #category t1 '
SET @SQLFINAL = @SQL + CHAR(13) + @SQLF + 'where t1.name = electronics and t1.name is not null'
While @@ROWCOUNT > 0
Begin
SET @SQLFINALP = @SQL + CHAR(13) + @SQLF + 'WHERE t1.name =''electronics'''
SET @CTP = @CT
Set @CT = @CT + 1
SET @SQL = @SQL +' , t' +@CT+'.name ''lev' + @CT + ''''
SET @SQLF = @SQLF + ' left join #category t' + @CT + ' on t' + @CTP + '.category_id = t' + @CT + '.parent '
SET @SQLFINAL = @SQL + 'into #temp' + CHAR(13) + @SQLF + 'where t' + @Ct + '.name is not null and t1.name = ''electronics'''
exec SP_EXECUTESQL @SQLFINAL
end
print @@ROWCOUNT
Select @CTP
print @SQLFINALp
exec SP_EXECUTESQL @SQLFINALp
rollback
source to share