How to do the Recursive SELECT query in MySQL?
Asked Answered
E

6

81

I got a following table:

col1 | col2 | col3
-----+------+-------
1    | a    | 5
5    | d    | 3
3    | k    | 7
6    | o    | 2
2    | 0    | 8

If a user searches for "1", the program will look at the col1 that has "1" then it will get a value in col3 "5", then the program will continue to search for "5" in col1 and it will get "3" in col3, and so on. So it will print out:

1   | a   | 5
5   | d   | 3
3   | k   | 7

If a user search for "6", it will print out:

6   | o   | 2
2   | 0   | 8

How to build a SELECT query to do that?

Elector answered 13/5, 2013 at 0:31 Comment(1)
There is a solution for your issue in this post https://mcmap.net/q/82533/-recursive-mysql-selectTorchier
C
70

Edit

Solution mentioned by @leftclickben is also effective. We can also use a stored procedure for the same.

CREATE PROCEDURE get_tree(IN id int)
 BEGIN
 DECLARE child_id int;
 DECLARE prev_id int;
 SET prev_id = id;
 SET child_id=0;
 SELECT col3 into child_id 
 FROM table1 WHERE col1=id ;
 create TEMPORARY  table IF NOT EXISTS temp_table as (select * from table1 where 1=0);
 truncate table temp_table;
 WHILE child_id <> 0 DO
   insert into temp_table select * from table1 WHERE col1=prev_id;
   SET prev_id = child_id;
   SET child_id=0;
   SELECT col3 into child_id
   FROM TABLE1 WHERE col1=prev_id;
 END WHILE;
 select * from temp_table;
 END //

We are using temp table to store results of the output and as the temp tables are session based we wont there will be not be any issue regarding output data being incorrect.

SQL FIDDLE Demo

Try this query:

SELECT 
    col1, col2, @pv := col3 as 'col3' 
FROM 
    table1
JOIN 
    (SELECT @pv := 1) tmp
WHERE 
    col1 = @pv

SQL FIDDLE Demo:

| COL1 | COL2 | COL3 |
+------+------+------+
|    1 |    a |    5 |
|    5 |    d |    3 |
|    3 |    k |    7 |

Note
parent_id value should be less than the child_id for this solution to work.

Consider answered 13/5, 2013 at 3:21 Comment(20)
It works like charm. Very simple but very effective. Thank you very much.Elector
People Pls mark this answer as an optimum solution since some other solutions of similar question (about Recursive Select in mysql) are quite complicated as it requires to create a table & insert data into it. This solution is very elegant.Elector
Just take one care with his solution, there is no cycle type dependency then it will go to infinite loop and one more thing it will only find 1 record of that col3 type so if there are multiple records then it won't work.Consider
@ Meherzad U r query works fine but my req. is that if I got any of the number from the series I should get all this list like if i get 3 then I have to traverse forward and reserves order as well How can we achieve that ? Please help me.....Protestantism
Can you post explain your question on fiddle. Im unable to understand your problem.Consider
Doesn't this solution depend on other elements you were not mentioning? For instance, once I set PK on col1 (like CREATE TABLE Table1 (col1 int, col2 varchar(1), col3 int, PRIMARY KEY (col1));), your query only return two rows while it should be three. For that matter, I tried hard for hours with my actual table that had a self reference, but it really didn't work and only return zero rows...Logos
This solution does indeed rely on certain conditions within the data, see my answer for details.Cohin
And where do I specify the ID of column where I want to start the recursion?Gaullism
@TomášZato If I have understood your question properly where col1=@pv is the case where we specify id from where recursion will start.Consider
@HamidSarfraz now it works sqlfiddle.com/#!2/74f457/14. This will work for you. As it goes for sequential search and id will always have greater value than parent, as parent needs to be created first. Pl inform if you need any extra details.Consider
This is not a solution. It's just a lucky side effect of a table scan. Read @Cohin 's answer carefully or you'll waste a lot of time as I did.Eskimoaleut
@Eskimoaleut Thanks for the pointer.. Ill add a note in my answer and the assumption on which the answer is based of having parent_id less than child_id. Please feel free to edit the answer.Consider
@Elector this solution is not elegant at all. It's just plain wrong.Alt
@ypercube, this question is to demonstrate how recursive SQL work, if you want to see a complete solution, please see explainextended.com/2009/03/17/hierarchical-queries-in-mysqlElector
Tum I know how recursive SQL works. MySQL has not implemented recursive CTEs, so one viable option is the one in the link you gave (using stored procedures/functions). Another is using mysql variables. However, the answer here is not elegant but the opposite, just horrible. It is not showing recursive SQL. If it worked in your case, in was only by accident, as @jaehung correctly pointed out. And I don't mind horrible answers. I just downvote them. But a horrible answer at +50, I do mind.Alt
@ypercube Thanks for raising this. I have updated the answer. Feel free to edit the answer.Consider
@ypercube, there is another way but it con only work with a small data. Please see #16542513Elector
@Consider What do you think about the new answer I posted ? Not that yours isn't good, but I wanted to have a SELECT onlyScandalize
@MasterDJon Looks good. It'll be helpful for the users if you can add working fiddle example.Consider
@Consider I added a fiddle as you suggested. BTW, yours doesn't work anymore, but if I remove '/1' from the URL, it loads.Scandalize
C
53

The accepted answer by @Meherzad only works if the data is in a particular order. It happens to work with the data from the OP question. In my case, I had to modify it to work with my data.

Note This only works when every record's "id" (col1 in the question) has a value GREATER THAN that record's "parent id" (col3 in the question). This is often the case, because normally the parent will need to be created first. However if your application allows changes to the hierarchy, where an item may be re-parented somewhere else, then you cannot rely on this.

This is my query in case it helps someone; note it does not work with the given question because the data does not follow the required structure described above.

select t.col1, t.col2, @pv := t.col3 col3
from (select * from table1 order by col1 desc) t
join (select @pv := 1) tmp
where t.col1 = @pv

The difference is that table1 is being ordered by col1 so that the parent will be after it (since the parent's col1 value is lower than the child's).

Cohin answered 23/7, 2014 at 4:30 Comment(5)
u right, also if a child has 2 parents, then it may not pick bothElector
Thanks man. Teamworek did its deed in this post! I got it to work when I changed the the value of @pv. That's what I was exactly looking for.Slimy
What if I want to use this as a group_concat column of parent IDs for each row in a bigger select (meaning that the value of @pv variable to be dynamic for each row). The join in subquery doesn't know the master column (on which I try to connect to), using another variable it doesn't work either (always returns NULL)Coequal
I have created a custom function which generates the tree path using group_concat, and I can now send as parameter the column value for each row ;)Coequal
What do you think about the new answer I posted ? Not that yours isn't good, but I wanted to have a SELECT only that could support parent id > child id.Scandalize
A
20

leftclickben answer worked for me, but I wanted a path from a given node back up the tree to the root, and these seemed to be going the other way, down the tree. So, I had to flip some of the fields around and renamed for clarity, and this works for me, in case this is what anyone else wants too--

item | parent
-------------
1    | null
2    | 1
3    | 1
4    | 2
5    | 4
6    | 3

and

select t.item_id as item, @pv:=t.parent as parent
from (select * from item_tree order by item_id desc) t
join
(select @pv:=6)tmp
where t.item_id=@pv;

gives:

item | parent
-------------
6    | 3
3    | 1
1    | null
Amari answered 25/9, 2015 at 5:16 Comment(3)
@BoB3K, would this work if the IDs are not necessarily in "order". It seems not to work in case a parent's id along the chain is higher then its child? E.g. chain 1 > 120 > 112 will only return ((112, 120)) while 2 > 22 > 221 returns the full chain ((221,22),(22,2),(2,null))Lout
It's been awhile, but I think I remember reading in the original answers that this does not work if the item ids are not in order, which usually isn't an issue if the id is an auto increment key.Amari
It works well and I use it for my site...the problem here is that is not possible to order the results ASC. 1 3 6 I use array_reverse() in php instead.....any sql solution for that?Deejay
G
9

Stored procedure is the best way to do it. Because Meherzad's solution would work only if the data follows the same order.

If we have a table structure like this

col1 | col2 | col3
-----+------+------
 3   | k    | 7
 5   | d    | 3
 1   | a    | 5
 6   | o    | 2
 2   | 0    | 8

It wont work. SQL Fiddle Demo

Here is a sample procedure code to achieve the same.

delimiter //
CREATE PROCEDURE chainReaction 
(
    in inputNo int
) 
BEGIN 
    declare final_id int default NULL;
    SELECT col3 
    INTO final_id 
    FROM table1
    WHERE col1 = inputNo;
    IF( final_id is not null) THEN
        INSERT INTO results(SELECT col1, col2, col3 FROM table1 WHERE col1 = inputNo);
        CALL chainReaction(final_id);   
    end if;
END//
delimiter ;

call chainReaction(1);
SELECT * FROM results;
DROP TABLE if exists results;
Gardia answered 27/1, 2014 at 10:37 Comment(1)
This is a robust solution and I am using it without trouble. Can you please help me when going in the other direction, i.e. down the tree - I find all rows where the parent id == inputNo, but many IDs may have one parent ID.Barrault
S
9

If you want to be able to have a SELECT without problems of the parent id having to be lower than child id, a function could be used. It supports also multiple children (as a tree should do) and the tree can have multiple heads. It also ensure to break if a loop exists in the data.

I wanted to use dynamic SQL to be able to pass the table/columns names, but functions in MySQL don't support this.

DELIMITER $$

CREATE FUNCTION `isSubElement`(pParentId INT, pId INT) RETURNS int(11)
DETERMINISTIC    
READS SQL DATA
BEGIN
DECLARE isChild,curId,curParent,lastParent int;
SET isChild = 0;
SET curId = pId;
SET curParent = -1;
SET lastParent = -2;

WHILE lastParent <> curParent AND curParent <> 0 AND curId <> -1 AND curParent <> pId AND isChild = 0 DO
    SET lastParent = curParent;
    SELECT ParentId from `test` where id=curId limit 1 into curParent;

    IF curParent = pParentId THEN
        SET isChild = 1;
    END IF;
    SET curId = curParent;
END WHILE;

RETURN isChild;
END$$

Here, the table test has to be modified to the real table name and the columns (ParentId,Id) may have to be adjusted for your real names.

Usage :

SET @wantedSubTreeId = 3;
SELECT * FROM test WHERE isSubElement(@wantedSubTreeId,id) = 1 OR ID = @wantedSubTreeId;

Result :

3   7   k
5   3   d
9   3   f
1   5   a

SQL for test creation :

CREATE TABLE IF NOT EXISTS `test` (
  `Id` int(11) NOT NULL,
  `ParentId` int(11) DEFAULT NULL,
  `Name` varchar(300) NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1;

insert into test (id, parentid, name) values(3,7,'k');
insert into test (id, parentid, name) values(5,3,'d');
insert into test (id, parentid, name) values(9,3,'f');
insert into test (id, parentid, name) values(1,5,'a');
insert into test (id, parentid, name) values(6,2,'o');
insert into test (id, parentid, name) values(2,8,'c');

EDIT : Here is a fiddle to test it yourself. It forced me to change the delimiter using the predefined one, but it works.

Scandalize answered 23/6, 2016 at 13:23 Comment(0)
L
0

Building off of Master DJon

Here is simplified function which provides the added utility of returning depth (in case you want to use logic to include the parent task or search at a specific depth)

DELIMITER $$
FUNCTION `childDepth`(pParentId INT, pId INT) RETURNS int(11)
    READS SQL DATA
    DETERMINISTIC
BEGIN
DECLARE depth,curId int;
SET depth = 0;
SET curId = pId;

WHILE curId IS not null AND curId <> pParentId DO
    SELECT ParentId from test where id=curId limit 1 into curId;
    SET depth = depth + 1;
END WHILE;

IF curId IS NULL THEN
    set depth = -1;
END IF;

RETURN depth;
END$$

Usage:

select * from test where childDepth(1, id) <> -1;
Leviticus answered 3/12, 2018 at 23:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.