Mysql hierarchy sql 계층구조 쿼리 : 설계 및 쿼리작성(Multi selectbox)
고객들이 열람할 가이드 정보가 있고,
가이드 정보의 카테고리를 N depth 로 분류(멀티 셀렉트박스 사용)하여 조회할 수 있게 하여 다음과 같은 화면을 만들어 보자.
<1: 테이블 설계>
1. '전체' 옵션의 유무
case 1 멀티 셀렉트박스에 '전체'라는 옵션이 없는 경우 :
마지막 셀렉트박스의 value(node_id)를 기준으로 가이드를 조회
case 2 멀티 셀렉트박스에 '전체'라는 옵션이 존재하는 경우 :
'전체'가 선택된 마지막 셀렉트박스 직전의 value(node_id)를 기준으로 모든 자손 node_id를 찾아 조회해야한다.
예를 들어,
case 1 ex) 고객지원 > 고객지원sub > 고객지원sub의sub 와 같이 마지막 select box에 특정 값이 선택된 경우 :
고객지원sub의sub 의 value(node_id) 를 기준으로 가이드 조회
case 2 ex) 고객지원 > 고객지원sub > 전체 와 같이 마지막 select box가 '전체'로 선택된 경우 :
고객지원sub을 부모 노드로 삼는 모든 자손 노드를 기준으로 가이드 조회
2. 동적 depth 여부
case 1 : depth 가 특정 값으로 고정인 경우, 셀렉트박스용 분류 코드테이블을 따로 두고 join 을 몇번 걸면 해결이 된다
case 2 : depth 가 동적(N)인 경우, join 이 N 번 되어야 하므로 단순한 join으로 해결 할 수 없다..
전체 옵션이 존재하고 depth 가 동적(N)인 경우를 전재하고 설계 및 개발해보자.
<2: 테이블 생성>
1. 메인테이블 생성 : 가이드 정보 테이블 생성
가이드 정보 테이블은 제목, 가이드의 노드 아이디, 상세 내용만 지니고 있다.
1
2
3
4
5
6
7
8
|
CREATE TABLE `guide_info` (
`SEQ` int(11) NOT NULL AUTO_INCREMENT COMMENT '일련번호',
`TITLE` varchar(100) NOT NULL COMMENT '제목',
`GUIDE_ID` int(11) NOT NULL COMMENT '가이드 노드 아이디',
`TEXT_ITEM_1` text COMMENT '가이드 내용',
PRIMARY KEY (`SEQ`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='고객지원 가이드 게시판';
|
cs |
2. 서브테이블 생성 : 가이드 분류 정보를 따로 지니고 있는 노드 정보 테이블 생성
1
2
3
4
5
6
7
8
9
|
CREATE TABLE `node_info` (
`NODE_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '노드아이디',
`PNODE_ID` int(11) NOT NULL COMMENT '부모노드아이디',
`NODE_PATH` varchar(200) DEFAULT NULL COMMENT '노드경로',
`NODE_NM` varchar(20) DEFAULT NULL COMMENT '노드명',
`DEPTH` int(11) DEFAULT '1' COMMENT '노드DEPTH',
`LEAF_YN` varchar(1) DEFAULT 'N' COMMENT '자식노드존재유무',
PRIMARY KEY (`NODE_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='노드 정보';
|
cs |
생성된 테이블 :
NODE_ID(노드아이디)와 PNODE_ID(부모 노드 아이디)가 서로 참조하는 관계의 테이블 구조.
샘플 데이터 :
<3: 쿼리 작성>
필요한 쿼리 종류는 크게 두가지로 나눠볼 수 있다.
1. 셀렉트박스의 구성요소인 노드정보(node_info TABLE)를 조회하는 쿼리 : 멀티셀렉트박스를 그리기 위한 데이터를 뽑는 쿼리
2. 선택된 셀렉트박스 값들을 기준으로 가이드 목록(guide_info TABLE)을 조회한 결과 : 조회 결과를 출력하기 위한 데이터를 뽑는 쿼리
3-1. 셀렉트 박스의 구성요소인 노드정보를 조회하는 쿼리 :
N번째(Ndepth) 셀렉트박스가 특정값으로 선택된 경우 N+1번째(N+1depth) 셀렉트박스 조회
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
SELECT node_id
, pnode_id
, node_nm
, depth
, leaf_yn
FROM node_info
WHERE 1=1
<choose>
<when test='node_id neq null and node_id neq ""'>
AND pnode_id = #{node_id}
</when>
<otherwise>
AND pnode_id = 1
</otherwise>
</choose>
|
cs |
* 8~15line : node_id 가 없는 경우, 아무것도 선택되지 않은 경우(화면에 최초 진입한 상태)이며 최고 조상이라 할 수 있는 node_id 1 을 기준으로 자식 노드를 구한다
3-2. 선택된 셀렉트박스 값들을 기준으로 가이드 목록을 조회한 결과
1) 마지막 셀렉트박스가 특정 값으로 선택된 경우 : 단순 join 으로 해결
2) 마지막 셀렉트박스가 '전체'로 선택된 경우 : 아래와 같이 해결
ORACLE 은 CONNECT BY 함수를 사용하여 트리구조의 데이터를 출력 할 수 있겠지만,
MYSQL 은 CONNECT BY 함수를 지원하지 않는다.
다음과 같이 특정 노드 밑에 있는 모든 자손, 특정 노드 상위에 있는 모든 조상 노드를 구할 수 있다.
1. 특정 노드의 자손 구하기
1
2
3
4
5
6
7
8
9
10
11
12
13
|
SELECT node_id
, pnode_id
, depth
, leaf_yn
, node_path
FROM (
SELECT *
FROM node_info
ORDER BY pnode_id, node_id
) node_sorted,
(SELECT @pv := #{node_id}) initial
WHERE FIND_IN_SET(pnode_id, @pv)
AND LENGTH(@pv := concat(@pv, ',', node_id))
|
cs |
2. 특정 노드의 조상 구하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
SELECT nid AS node_id
, pnode_id
, leaf_yn
, depth
, node_path
FROM (
SELECT @nid AS nid,
(
SELECT @nid := pnode_id
FROM node_info
WHERE node_id = nid
) AS pnode_id
, leaf_yn
, depth
, node_path
FROM (
SELECT @nid := #{node_id}
) vars
, node_info
WHERE @nid <> 0
) as tmp
|
cs |
3. 특정 분류 밑의 모든 가이드 목록 조회 (특정 노드아이디 기준 모든 자손 노드를 구하는 1번 쿼리를 sub query로 사용)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
SELECT seq,
title,
(SELECT node_path FROM node_info where node_id = gi.guide_id) as guide_path
FROM guide_info gi
WHERE 1=1
AND gi.guide_id IN (SELECT * FROM (
SELECT node_id
FROM (SELECT node_id, pnode_id
FROM node_info
ORDER BY pnode_id, node_id
) node_sorted,
(select @pv := #{node_id}) initial
WHERE FIND_IN_SET(pnode_id, @pv)
AND LENGTH(@pv := concat(@pv, ',', node_id))
) as temp
)
ORDER BY seq
|
cs |
※ 위와 같이 subquery 를 사용하여 sql 에서 처리하지 않고, 소스레벨에서 처리하고 싶은 경우 foreach 를 사용하여 다음과 같이 처리할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
SELECT gi.title, gi.guide_id, gi.text_item_1, ni.node_path AS rs
FROM guide_info gi
, node_info ni
WHERE 1=1
AND ni.node_id = gi.guide_id
<if test = 'sch_word neq null and sch_word neq ""'>
AND title LIKE CONCAT('%',#{sch_word},'%')
</if>
<if test = 'totalSearchFlag neq null and totalSearchFlag neq ""'>
<choose>
<when test = 'totalSearchFlag eq "Y"'>
AND ni.node_id IN
<foreach item="node_id_grp" index="i" collection="node_id_grp" separator="," open="(" close=")">
#{node_id_grp}
</foreach>
</when>
<otherwise>
AND ni.node_id = #{node_id}
</otherwise>
</choose>
</if>
|
cs |
* totalSearchFlag 는 selectBox 에서 '전체'가 선택되었음을 판별하기 위해 사용되는 값
* 1번 쿼리(특정노드의 모든 자손 구하기)로 뽑아온 모든 자손 노드를 필드명이 node_id_grp 인 List 자료형 에 담아 sql mapper 로 넘겨주어 사용
* open 은 for 문이 맨처음 돌기 전에, close 는 for문이 끝까지 돌고나서, separator 는 for 문이 한 번 씩 돌 때마다 적용된다.
* foreach(for문)로 작성된 쿼리 결과물은 다음과 같다
(생략) ~ AND ni.node_id IN (2, 6)
위 쿼리를 사용해 가져온 데이터를 화면에 그려보자.
'DB > MYSQL' 카테고리의 다른 글
JOIN 에서 WHERE 와 ON 의 차이, 그리고 OUTER JOIN (4) | 2019.11.14 |
---|---|
SYSDATE vs NOW (SYSDATE 와 NOW 의 차이) (0) | 2019.11.02 |
[Mysql] 시간 계산 : DATE_ADD, DATE_SUB (0) | 2019.08.21 |
SELECT + INSERT (mysql + mybatis) (0) | 2019.06.12 |
[Mysql/ORACLE] 여러 행을 한 줄로 표현하기(GROUP_CONCAT) (0) | 2019.05.27 |