Hierarchical data in MySQL: parents and children in one query | EXPLAIN EXTENDED
IT노트/MYSQL 2015. 2. 16. 00:21Hierarchical data in MySQL: parents and children in one query
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:
- Original hierarchical query that returns all descendants of a given
id
(a descendancy chain) - 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
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
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
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 level
s 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
'IT노트 > MYSQL' 카테고리의 다른 글
[mysql] function 정보 (0) | 2015.02.20 |
---|---|
[MySQL] DB 캐릭터셋 확인하기 (0) | 2015.02.19 |
MySQL 프로시저(Procedure) (0) | 2015.02.16 |
Incorrect string value 에러 : alter TABLE INC_ACT_INFO modify column ACT_CONTENT text character set utf8 collate utf8_general_ci; (0) | 2015.02.16 |
참고 DB 생성 (0) | 2015.02.14 |