Recently for a student, I was asked to explain the design considerations of a Binary Tree which was to be used in an MLM solution. About 10 years back it was a nightmare, and in my career, I was lucky to get that privilage for more than a dozen times with varying schemes and structures. But now with MySQL having procedures, and functions, the tree design and related functions were a breze. Reproducing it here for future reference. This is in no way a complete solution, but just bits and pieces which may even be discarded as crap.
CREATE TABLE `binTree` ( `nodeid` int(10) unsigned NOT NULL auto_increment, `lnode` int(10) unsigned NOT NULL default '0', `rnode` int(10) unsigned NOT NULL default '0', `pnode` int(10) unsigned NOT NULL default '0', `pside` enum('l','r') NOT NULL default 'l', `tLevel` int(10) unsigned NOT NULL default '1', PRIMARY KEY (`nodeid`), KEY `parent` (`pnode`), KEY `treelevel` (`tLevel`), KEY `lside` (`lnode`), KEY `rside` (`rnode`) ) ENGINE=MyISAM;
The above is the basic structure of the tree table, and some complementary functions and procedures are accompanied to make the usage simple, otherwise would be a herculian task for the developer to do the same.
CREATE PROCEDURE `addTreeNode`(iParent int, cSide char(1)) BEGIN declare iCheck int; declare iLevel int; declare iNewNode int; select if(@silentmode is null, 0, @silentmode) into @silentmode; # need to do some validations, since this is add.. and not change # check that parent side is vaccant if cSide = 'l' then select lnode into iCheck from binTree where nodeid = iParent; end if; if cSide = 'r' then select rnode into iCheck from binTree where nodeid = iParent; end if; if iCheck = 0 then # okay we can start applying select tLevel into iLevel from binTree where nodeid = iParent; insert into binTree ( `pnode`, `pside`, `tLevel`) values (iParent, cSide, iLevel + 1); select last_insert_id() into iNewNode; if cSide = 'l' then update binTree set lnode = iNewNode where nodeid = iParent; end if; if cSide = 'r' then update binTree set rnode = iNewNode where nodeid = iParent; end if; else set iNewNode = 0; end if; if @silentmode = 0 then select iNewNode as res; end if; END
The addTreeNode procedure is called to add a new node into the system, if need to use this, one should have a login, and profile table different, and then invoke the procedure with parentid and l/r for left/right, which will return the userid, which should be used as primary key to save login info as well as contact info.
CREATE PROCEDURE `getTreeFrom`(iNode int) BEGIN declare iMaxLevel int; declare iLevel int; declare iCheck int; select tLevel + 1 into iLevel from binTree where nodeid = iNode; select max(tLevel) into iMaxLevel from binTree; # cant play without temporary tables; create temporary table tNodes (itNode int unsigned ); create temporary table uNodes (itNode int unsigned ); insert into tNodes value(iNode); loop1: LOOP if iLevel > iMaxLevel then LEAVE loop1; end if; insert into uNodes select nodeid from binTree where pnode in (select itNode from tNodes) and tLevel = iLevel; select count(*) into iCheck from uNodes; if iCheck = 0 then LEAVE loop1; end if; insert into tNodes select itNode from uNodes; truncate table uNodes; set iLevel = iLevel + 1; END LOOP; select bt.* from binTree bt, tNodes tn where tn.itNode = bt.nodeid order by bt.tLevel, bt.nodeid; drop table uNodes; drop table tNodes; END
This procedure would be a bit too much on systems when the tree called from is one of the top range.. still we cannot let the system go without one like this and hence any tree starting from a node can be selected using getTreeFrom.
CREATE PROCEDURE `pruneNode`(iNode int, iParent int, sSide char(1)) BEGIN declare iCheck int; declare iNewLevel int; declare oParent int; declare oSide char(1); #request is to pluck iNode from existing parent and attach to new parent in said side #side should not be empty.. check that first if (sSide = 'l') then select lnode into iCheck from binTree where nodeid = iParent; if iCheck = 0 then update binTree set lnode = iNode where nodeid = iParent; end if; end if; if (sSide = 'r') then select rnode into iCheck from binTree where nodeid = iParent; if iCheck = 0 then update binTree set rnode = iNode where nodeid = iParent; end if; end if; if (iCheck > 0) then select 0 as res; else select pnode into oParent from binTree where nodeid = iNode; select pside into oSide from binTree where nodeid = iNode; update binTree set pnode=iParent, pside=sSide where nodeid = iNode; if oSide = 'l' then update binTree set lnode = 0 where nodeid = oParent; end if; if oSide = 'r' then update binTree set rnode = 0 where nodeid = oParent; end if; select tLevel into iNewLevel from binTree where nodeid = iNode; CALL stabilizeLevels(iNewLevel); end if; END
This one function was just out of the nostalgy, since this was demanded by one of my clients, and we just said it was not possible thinking of all the recursive calls we had to implement using php, but now just a wimph.. The parameters are node to be pruned, new parent, and side of new parent. Resulting result set will contain single column single row, either ‘0’ ( means the new parent’s side was not empty ) or ‘1’ ( means the operation was successful ). This uses a complementary procedure for which the listing is reproduced below.
CREATE PROCEDURE `stabilizeLevels`(iLevel int) BEGIN declare iCheck int; declare iMaxLevel int; declare iNCount int; declare sNodeList text; if iLevel = 1 then select 0 as res; else #starting from iLevel.. traverse to iMaxLevel and make sure all nodes are correct.. select max(tLevel) into iMaxLevel from binTree; loop1: LOOP if (iLevel > iMaxLevel) then LEAVE loop1; end if; create TEMPORARY table mylist (iNode int); insert into mylist select nodeid from binTree where tLevel = iLevel; select sum(checkLevel(iNode) * 1) into iCheck from mylist; drop table mylist; if (iCheck = 0) then LEAVE loop1; end if; set iLevel = iLevel + 1; END LOOP; select 1 as res; end if; END
And another function which was just out of sheer curiosity..
CREATE FUNCTION `checkLevel`(iNode int) RETURNS enum('0','1') CHARSET latin1 BEGIN declare iParent int; declare iMyLevel int; declare iCheck int; declare iMaxLevel int; if (iNode = 0) then return '0'; end if; #get parent level and make sure the difference is 1 if not.. go deep and update the levels select count(*) into iCheck from binTree where nodeid = iNode; if iCheck = 0 then return '0'; end if; select tLevel into iMyLevel from binTree where nodeid = iNode; select pnode into iParent from binTree where nodeid = iNode; select iMyLevel - tLevel into iCheck from binTree where nodeid = iParent; if iCheck <> 1 then select tLevel + 1 into iMyLevel from binTree where nodeid = iParent; update binTree set tLevel = iMyLevel where nodeid = iNode; return '1'; else return '0'; end if; END
The above system is not benchmarked, neither tested in realtime. But if it becomes of use to any one of you, please add a comment to the comments box here.
Sunday, Oct 4.
Just added a couple of procedures more to test and do some benchmarks. Adding those procedures to this post, for future reference.
CREATE PROCEDURE `initTree`() BEGIN truncate table binTree; insert into binTree values (1,0,0,0,'l',1); select * from binTree; END
Just got tired of truncating the table, inserting the initial row etc..
CREATE PROCEDURE `gFillNodes`(iParent int, iCount int, bRandPlace binary(1)) BEGIN declare sLeg char(1); declare iTest int; declare iLevel int; #start the loop set @silentmode = 1; loop1: LOOP if iCount = 0 then LEAVE loop1; end if; if bRandPlace = 1 then select floor(rand() * 100) % 2 into iTest; if iTest = 0 then set sLeg = 'r'; select min(nodeid) into iParent from binTree where nodeid >= iParent and rnode = 0; else set sLeg = 'l'; select min(nodeid) into iParent from binTree where nodeid >= iParent and lnode = 0; end if; else select min(tLevel) into iLevel from binTree where nodeid >= iParent and (lnode = 0 or rnode = 0); select count(*) into iTest from binTree where lnode = 0 and tLevel = iLevel; if iTest = 0 then select min(nodeid) into iParent from binTree where rnode = 0 and tLevel = iLevel; set sLeg = 'r'; else select min(nodeid) into iParent from binTree where lnode = 0 and tLevel = iLevel; set sLeg = 'l'; end if; end if; CALL addTreeNode(iParent, sLeg); set iCount = iCount - 1; END LOOP; END
Random as well as sequential fill of the tree.. one can see the working in the screen shot of the console..
Addition
Few days back some body was asking me, how I would go about to finding the parent node of parent node of given node, well I did not have to think much, than kick in the following listing as a mysql function. Since mysql does not support arrays (afaik), well that is not a big problem, we could split the return value in any of the programming language.
CREATE FUNCTION `getAncestors`(iNode int, iSteps int) RETURNS text BEGIN declare sAncestors text; if (iSteps = 0) then select count(*) into iSteps from binTree where nodeId <= iNode; end if; loop1: LOOP if ( iSteps <= 0 ) then LEAVE loop1; end if; select pnode into iNode from binTree where nodeid = iNode; if (iNode = 0) then LEAVE loop1; end if; if(length(sAncestors) > 0) then set sAncestors = concat(sAncestors,',',iNode); else set sAncestors = concat(iNode,''); end if; set iSteps = iSteps - 1; END LOOP; RETURN sAncestors; END