Binary tree in MySQL for MLM

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..
proc_work

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