1

I am using this procedure that I created to create a member's downline.

PROCEDURE get_downline(IN id INT)
BEGIN
    declare cur_depth int default 1;

    -- Create the structure of the final table
    drop temporary table if exists tmp_downline;
    create temporary table tmp_downline (
        member_id int unsigned,
        referrer_id int unsigned,
        depth tinyint unsigned
    );

    -- Create a table for the previous list of users
    drop temporary table if exists tmp_members;
    create temporary table tmp_members(
        member_id int unsigned
    );

    -- Make a duplicate of tmp_members so we can select on both
    drop temporary table if exists tmp_members2;
    create temporary table tmp_members2(
        member_id int unsigned
    );

    -- Create the level 1 downline
    insert into tmp_downline select id, member_id, cur_depth from members where referrer_id = id;
    -- Add those members into the tmp table
    insert into tmp_members select member_id from members where referrer_id = id;

    myLoop: while ((select count(*) from tmp_members) > 0) do
        -- Set next level of users
        set cur_depth = cur_depth + 1;

        -- Insert next level of users into table
        insert into tmp_downline select id, member_id, cur_depth from members where referrer_id in(select member_id from tmp_members);

        -- Re-fill duplicate temporary table
        truncate table tmp_members2;
        insert into tmp_members2 select member_id from tmp_members;

        -- Reset the default temporary table
        truncate table tmp_members;
        insert into tmp_members select member_id from members where referrer_id in(select member_id from tmp_members2);

    end while;

    -- Get the final list of results
    select * from tmp_downline order by depth;
END

Here are my results:

Results

Found rows: 424,097; Duration for 1 query: 12.438 sec.

All the queries look like they are using optimized indexes, but it is still taking a while to run. Is there a better way to run my while loop? I feel that making 2 temporary tables might be part of the issue, but when running my last insert query I can't reopen the temporary table which is why I made a duplicate table.

Here is a slimmed down version of the original table (original has 50 cols):

CREATE TABLE `members` (
    `member_id` INT(11) NOT NULL AUTO_INCREMENT,
    `referrer_id` INT(11) NOT NULL DEFAULT '0',
    PRIMARY KEY (`member_id`),
    INDEX `referrer_id_idx` (`referrer_id`)
);

What I am trying to achieve is to get an MLM downline.

Here is a picture that shows a downline where the number shows the level and you are the main circle at the top.

Level 1: People you referred to the program
Level 2: People your referrals referred to the program
Level 3: People your referrals, referrals referred to the program
Level 4: ...
Level 5: ....
Level ........

MLM Preview

Get Off My Lawn
  • 34,175
  • 38
  • 176
  • 338
  • Anytime you're writing loops in SQL it can probably be done better. I think we'd be more able to help if you showed your schema and explained what you're trying to accomplish. – Schwern Jan 12 '16 at 21:54
  • @Schwern I have updated the question with extra information – Get Off My Lawn Jan 12 '16 at 22:00
  • Thanks. Could you link to an explanation of what an MLM downline is? I think you're doing a SQL tree traversal. – Schwern Jan 12 '16 at 22:02
  • I am guessing he wants a list of everyone a member has referred, and who they've referred has referred, and so on... – Uueerdo Jan 12 '16 at 22:04
  • 1
    Possible duplicate of [Is it possible to query a tree structure table in MySQL in a single query, to any depth?](http://stackoverflow.com/questions/169817/is-it-possible-to-query-a-tree-structure-table-in-mysql-in-a-single-query-to-an). I think, rather than trying to repair that procedure, you should look into the general answers about dealing with tree structures in SQL. – Schwern Jan 12 '16 at 22:10
  • @Schwern I can not change the database structure. – Get Off My Lawn Jan 12 '16 at 22:12
  • You don't have to. It's already a tree. Instead of parents knowing their children, each child (member) knows their parent (referrer). This is a common way to model a tree in SQL. – Schwern Jan 12 '16 at 22:12
  • But what they are doing is adding a "right" and a "left" column – Get Off My Lawn Jan 12 '16 at 22:14
  • There are multiple techniques presented, some for your structure. – Schwern Jan 13 '16 at 19:52

2 Answers2

0

Re-evaluating select count(*) from tmp_members for each iteration is expensive and unnecessary. Find the initial count then deduct mysql_affected_rows() after each operation. Doing a proper join in the the insert....select...in (select...) will likely result in better plans.

You've not shown the structure of the members table nor the EXPLAIN plans for the queries, so its impossible to say if there's any scope for optimising there, but you've probably get a big performance uplift by using a proper graph database rather than a relational one for this task. Or maintain a denormalised view of the results populated when you add a new record to the base table. Or use a more appropriate tree representation such as an adjacency list.

symcbean
  • 47,736
  • 6
  • 59
  • 94
0

I think you might be able to do away with the second tmp_members table...

PROCEDURE get_downline(IN id INT)
BEGIN
    declare cur_depth int default 1;

    -- Create the structure of the final table
    drop temporary table if exists tmp_downline;
    create temporary table tmp_downline (
        member_id int unsigned,
        referrer_id int unsigned,
        depth tinyint unsigned
    );

    -- Create a table for the previous list of users
    drop temporary table if exists tmp_members;
    create temporary table tmp_members(
        member_id int unsigned
    );

    -- Create the level 1 downline
    insert into tmp_downline 
        select id, member_id, cur_depth 
        from members 
        where referrer_id = id
    ;

    myLoop: while (mysql_affected_rows()>1) do

        -- Load tmp_members with previously added referred members
        -- to use as the next potential referrers
        truncate tmp_members;
        insert into tmp_members 
            select referrer_id 
            from tmp_downline 
            where depth = cur_depth
        ;

        -- Set next level of users
        set cur_depth = cur_depth + 1;

        -- Insert next level of users into table
        insert into tmp_downline 
            select id, m.member_id, cur_depth 
            from members AS m
            INNER JOIN tmp_members AS t ON m.referrer_id = t.member_id
        ;

    end while;

    -- Get the final list of results
    select * from tmp_downline order by depth;
END

In this case, tmp_members could be more appropriately named tmp_candidate_referrers, but I left it as it was to show how little of a change it was.

Also, for this version, you might consider adding indexes for tmp_downline.cur_depth and tmp_members.member_id since they are repeatedly used.

--- Edit ---

I made a correction in the first insert within the loop above. referrer_id should be selected from tmp_downline, not member_id; which brings me to a question...

Why bother with the tmp_downline.member_id field when it always contains the value of the argument passed?

If you need it in the final results, you can always finish with

SELECT id AS member_id, referrer_id, depth FROM tmp_downline ORDER BY depth;
Uueerdo
  • 15,723
  • 1
  • 16
  • 21
  • The first insert within the while loop is not going to work because depth needs to be reset to `1` for each member. – Get Off My Lawn Jan 13 '16 at 15:08
  • @GetOffMyLawn The depth should not need reset. You've already found the previous referrals' referrals in the previous iteration. However, an interesting point has occurred to me; if your database has circular referrals this is never going to end. – Uueerdo Jan 13 '16 at 17:23
  • well depth has to reset. everyone has a level 1 unless your thinking of this differently than I am where you build everyone's level one at once, where I am not doing that. I am building everyone's full level depth one at a time... – Get Off My Lawn Jan 13 '16 at 17:26
  • @GetOffMyLawn Regarding the non-terminating loop... adding a unique key on `tmp_downline.referred_id`and using an `INSERT IGNORE` for `tmp_downline` may solve it. Regarding depth, what you are doing is building **one** member's full depth, one "layer" at a time; the same as my answer. A member's "depth 1" referrals are their referrer's "depth 2" ones, and so on. – Uueerdo Jan 13 '16 at 17:31
  • @GetOffMyLawn Actually, I think I have spotted a another flaw in yours. Since you check everyone in the referral "tree" every iteration and apply the next depth, you are going to get referred members duplicated at every level after the first they appear in... maybe, nope, nevermind, you are just getting the last "layer" in a different way. – Uueerdo Jan 13 '16 at 17:34