Hierarchical data in MySQL: parents and children in one query

with 3 comments

Answering questions asked on the site.

Michael asks:

I was wondering how to implement a hierarchical query in MySQL (using the ancestry chains version) for a single row, such that it picks up the parents (if any) and any children (if any).

The idea is, I want to be able to jump in at any point, provide an Id of some sort, and be able to draw out the entire hierarchy for that Id, both upwards and downwards.

We need to combine two queries here:

  1. Original hierarchical query that returns all descendants of a given id (a descendancy chain)
  2. A query that would return all ancestors of a given id (an ancestry chain)

An id can have only one parent, that’s why we can employ a linked list technique to build an ancestry chain, like shown in this article:

Here’s the query to to this (no functions required):

01.SELECT  @r AS _id,
02.(
03.SELECT  @r := parent
04.FROM    t_hierarchy
05.WHERE   id = _id
06.) AS parent,
07.@l := @l + 1 AS lvl
08.FROM    (
09.SELECT  @r := 1218,
10.@l := 0,
11.@cl := 0
12.) vars,
13.t_hierarchy h
14.WHERE    @r <> 0

To combine two queries, we can employ a simple UNION ALL.

The only problem that is left to preserve the correct level, since the ancestry chain query conts level backwards, and the hierarchical query will count it starting from zero.

Let’s create a sample table and see what we get:

Table creation details

Now, let’s try to UNION ALL the queries as is:

01.SELECT  CONCAT(REPEAT('    ', lvl  - 1), _id) AS treeitem, parent, lvl AS level
02.FROM    (
03.SELECT  @r AS _id,
04.(
05.SELECT  @r := parent
06.FROM    t_hierarchy
07.WHERE   id = _id
08.) AS parent,
09.@l := @l + 1 AS lvl
10.FROM    (
11.SELECT  @r := 1218,
12.@l := 0,
13.@cl := 0
14.) vars,
15.t_hierarchy h
16.WHERE   @r <> 0
17.ORDER BY
18.lvl DESC
19.) qi
20.UNION ALL
21.SELECT  CONCAT(REPEAT('    ', level  - 1), CAST(hi.id AS CHAR)), parent, level
22.FROM    (
23.SELECT  hierarchy_connect_by_parent_eq_prior_id(id) AS id, @level AS level
24.FROM    (
25.SELECT  @start_with := 1218,
26.@id := @start_with,
27.@level := 0
28.) vars, t_hierarchy
29.WHERE   @id IS NOT NULL
30.) ho
31.JOIN    t_hierarchy hi
32.ON      hi.id = ho.id
treeitemparentlevel
106
215
724
3873
165382
12181651
547312181
2479654732
2479754732
2479854732
2479954732
2480054732
547412181
2480154742
2480254742
2480354742
2480454742
2480554742
547512181
2480654752
2480754752
2480854752
2480954752
2481054752
547612181
2481154762
2481254762
2481354762
2481454762
2481554762
547712181
2481654772
2481754772
2481854772
2481954772
2482054772
36 rows fetched in 0.0014s (0.1447s)

We see that the hierarchical order is mangled: the first resultset is upside down, the second one is starting from level = 1.

To fix it, we need to change the code that calculates level a little.

First, we need to reverse the ancestry part.

This can be easily done by sorting it on lvl DESC:

01.SELECT  CONCAT(REPEAT('    ', level  - 1), _id) AS treeitem, parent, level
02.FROM    (
03.SELECT  @r AS _id,
04.(
05.SELECT  @r := parent
06.FROM    t_hierarchy
07.WHERE   id = _id
08.) AS parent,
09.@l := @l + 1 AS level
10.FROM    (
11.SELECT  @r := 1218,
12.@l := 0,
13.@cl := 0
14.) vars,
15.t_hierarchy h
16.WHERE   @r <> 0
17.ORDER BY
18.level DESC
19.) qi
treeitemparentlevel
106
215
724
3873
165382
12181651
6 rows fetched in 0.0003s (0.0684s)

We now have it in correct order but with wrong level values.

Since a level is essentially a rownum here, we can just calculate it as a rownum instead:

01.SELECT  CONCAT(REPEAT('    ', level  - 1), id) AS treeitem, parent, level
02.FROM    (
03.SELECT  _id AS id, parent,
04.@cl := @cl + 1 AS level
05.FROM    (
06.SELECT  @r AS _id,
07.(
08.SELECT  @r := parent
09.FROM    t_hierarchy
10.WHERE   id = _id
11.) AS parent,
12.@l := @l + 1 AS level
13.FROM    (
14.SELECT  @r := 1218,
15.@l := 0,
16.@cl := 0
17.) vars,
18.t_hierarchy h
19.WHERE   @r <> 0
20.ORDER BY
21.level DESC
22.) qi
23.) qo
treeitemparentlevel
101
212
723
3874
165385
12181656
6 rows fetched in 0.0003s (0.0712s)

We disregard the previously selected level at all, leaving it only inside the inline view qi for ordering purposes.

Now, we need to merge the descendancy tree query, but with levels starting from 6, not from 1.

Since this query will come after the ancestry one, we can use accumulated value of @cl (which we used to calculate level in the previous query) as a seed.

To do that, we can take the level returned by the descendancy tree query, and just add @cl to it.

The only problem it to determine when we should increment @cl and when we should add it to level.

We will just use a boolean field which will help us to tell the sets apart.

Here’s the query to do it:

01.SELECT  CONCAT(REPEAT('    ', level - 1), CAST(id AS CHAR)),
02.parent,
03.level
04.FROM    (
05.SELECT  id, parent, IF(ancestry, @cl := @cl + 1, level + @cl) AS level
06.FROM    (
07.SELECT  TRUE AS ancestry, _id AS id, parent, level
08.FROM    (
09.SELECT  @r AS _id,
10.(
11.SELECT  @r := parent
12.FROM    t_hierarchy
13.WHERE   id = _id
14.) AS parent,
15.@l := @l + 1 AS level
16.FROM    (
17.SELECT  @r := 1218,
18.@l := 0,
19.@cl := 0
20.) vars,
21.t_hierarchy h
22.WHERE   @r <> 0
23.ORDER BY
24.level DESC
25.) qi
26.UNION ALL
27.SELECT  FALSE, hi.id, parent, level
28.FROM    (
29.SELECT  hierarchy_connect_by_parent_eq_prior_id(id) AS id, @level AS level
30.FROM    (
31.SELECT  @start_with := 1218,
32.@id := @start_with,
33.@level := 0
34.) vars, t_hierarchy
35.WHERE   @id IS NOT NULL
36.) ho
37.JOIN    t_hierarchy hi
38.ON      hi.id = ho.id
39.) q
40.) q2
CONCAT(REPEAT(‘ ‘, level – 1), CAST(id AS CHAR))parentlevel
101
212
723
3874
165385
12181656
547312187
2479654738
2479754738
2479854738
2479954738
2480054738
547412187
2480154748
2480254748
2480354748
2480454748
2480554748
547512187
2480654758
2480754758
2480854758
2480954758
2481054758
547612187
2481154768
2481254768
2481354768
2481454768
2481554768
547712187
2481654778
2481754778
2481854778
2481954778
2482054778
36 rows fetched in 0.0016s (0.1514s)


Posted by wychoi
,