[DB] 데이터베이스 개념노트 22편 - 계층형 질의 START WITH, CONNECT BY PRIOR

데이터베이스 개념노트 22편 - 계층형 질의 START WITH, CONNECT BY PRIOR

지난 글에서는 정렬 결과 중 상위 N개 데이터를 조회하는 Top N 쿼리, ROWNUM, FETCH FIRST에 대해 정리했습니다.

이번 글에서는 SQLD에서 빠뜨리면 안 되는 Oracle SQL 핵심 문법인 계층형 질의에 대해 정리해 보겠습니다.

계층형 질의는 조직도, 메뉴 구조, 카테고리 구조처럼 상위 데이터와 하위 데이터가 연결된 구조를 조회할 때 사용합니다. SQLD에서는 START WITH, CONNECT BY PRIOR, LEVEL, ORDER SIBLINGS BY 같은 키워드를 잘 이해해야 합니다.


1. 계층형 질의란?

계층형 질의는 부모와 자식 관계를 가진 데이터를 계층 구조로 조회하는 SQL입니다.

계층형 질의는 테이블 안의 행들이 상하 관계를 가질 때, 루트부터 하위 노드까지 트리 구조로 조회하는 방법이다.

대표적인 예시는 다음과 같습니다.

  • 회사 조직도
  • 게시판 댓글과 대댓글
  • 상품 카테고리
  • 웹사이트 메뉴 구조
  • 폴더와 하위 폴더 구조

2. 계층 구조 예시

회사 조직도를 예로 들어보겠습니다.

대표
 ├─ 개발팀장
 │   ├─ 백엔드개발자
 │   └─ 프론트엔드개발자
 └─ 기획팀장
     └─ 서비스기획자

이 구조에서 대표는 최상위 노드이고, 개발팀장과 기획팀장은 대표의 하위 노드입니다. 백엔드개발자와 프론트엔드개발자는 개발팀장의 하위 노드입니다.


3. 계층형 질의에서 사용하는 용어

용어 설명 예시
루트 노드 계층 구조의 최상위 데이터 대표
부모 노드 다른 데이터를 아래에 가지는 상위 데이터 개발팀장
자식 노드 부모 아래에 연결된 하위 데이터 백엔드개발자
리프 노드 더 이상 자식이 없는 마지막 데이터 백엔드개발자, 프론트엔드개발자
LEVEL 계층의 깊이 대표는 LEVEL 1

4. 예제 테이블

이번 글에서는 다음 사원 테이블을 기준으로 계층형 질의를 살펴보겠습니다.

사원번호 사원명 직급 관리자번호
E001 김대표 대표 NULL
E002 이팀장 개발팀장 E001
E003 박팀장 기획팀장 E001
E004 최개발 백엔드개발자 E002
E005 정개발 프론트엔드개발자 E002
E006 한기획 서비스기획자 E003

여기서 사원번호는 각 사원을 구분하는 값이고, 관리자번호는 해당 사원의 상위 관리자를 나타냅니다.


5. 계층형 질의 기본 문법

Oracle 계층형 질의는 보통 다음 구조로 작성합니다.

SELECT 컬럼명
FROM 테이블명
START WITH 시작조건
CONNECT BY PRIOR 부모컬럼 = 자식컬럼;
구문 역할
START WITH 계층 탐색을 시작할 루트 노드를 지정한다.
CONNECT BY 부모와 자식 행을 연결하는 조건을 지정한다.
PRIOR 부모 행의 컬럼을 의미한다.
LEVEL 현재 행의 계층 깊이를 나타낸다.

6. START WITH란?

START WITH는 계층 구조를 어디서부터 시작할지 지정하는 구문입니다.

START WITH는 계층형 질의에서 루트 노드를 지정하는 조건이다.

조직도에서는 보통 최상위 관리자인 대표부터 시작합니다. 대표는 관리자번호가 NULL입니다.

START WITH 관리자번호 IS NULL

이 조건은 관리자번호가 없는 최상위 사원을 루트 노드로 삼겠다는 의미입니다.


7. CONNECT BY란?

CONNECT BY는 부모 행과 자식 행을 어떻게 연결할지 지정하는 구문입니다.

CONNECT BY는 계층 구조에서 상위 행과 하위 행의 연결 조건을 정의한다.

사원 테이블에서는 부모의 사원번호가 자식의 관리자번호와 연결됩니다.

CONNECT BY PRIOR 사원번호 = 관리자번호

이 조건은 다음 의미입니다.

부모 행의 사원번호 = 자식 행의 관리자번호

즉, 상위 사원의 사원번호를 관리자번호로 가진 하위 사원을 찾아 내려갑니다.


8. PRIOR란?

PRIOR는 계층형 질의에서 부모 행의 컬럼을 가리키는 키워드입니다.

PRIOR가 붙은 컬럼은 부모 행의 값을 의미한다.

다음 조건을 다시 보겠습니다.

CONNECT BY PRIOR 사원번호 = 관리자번호

여기서 PRIOR는 사원번호 앞에 붙어 있습니다. 따라서 이 조건은 부모 행의 사원번호와 자식 행의 관리자번호를 비교합니다.

표현 의미
PRIOR 사원번호 부모 행의 사원번호
관리자번호 자식 행의 관리자번호

9. 위에서 아래로 조회하기

대표부터 시작해서 하위 사원으로 내려가는 계층형 질의를 작성해 보겠습니다.

SELECT LEVEL,
       사원번호,
       사원명,
       직급,
       관리자번호
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호;

이 SQL은 관리자번호가 NULL인 김대표부터 시작해서, 김대표를 관리자로 가지는 사원, 그 사원을 관리자로 가지는 사원을 순서대로 조회합니다.

즉, 조직도를 위에서 아래로 내려가며 조회합니다.


10. LEVEL이란?

LEVEL은 계층형 질의에서 현재 행이 몇 번째 깊이에 있는지를 나타내는 가상 컬럼입니다.

LEVEL은 루트 노드를 1로 시작하여 하위 단계로 내려갈수록 1씩 증가하는 계층 깊이 값이다.

예제 조직도에서는 다음과 같이 볼 수 있습니다.

LEVEL 사원명 직급
1 김대표 대표
2 이팀장 개발팀장
2 박팀장 기획팀장
3 최개발 백엔드개발자
3 정개발 프론트엔드개발자

11. 계층 구조 들여쓰기 출력

LEVEL을 활용하면 계층 구조를 보기 좋게 들여쓰기해서 출력할 수 있습니다.

SELECT LEVEL,
       LPAD(' ', (LEVEL - 1) * 2) || 사원명 AS 조직도,
       직급
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호;

LEVEL이 깊어질수록 앞에 공백을 붙여 조직도처럼 보이게 만드는 예시입니다.

김대표
  이팀장
    최개발
    정개발
  박팀장
    한기획

12. 아래에서 위로 조회하기

계층형 질의는 위에서 아래로만 조회하는 것이 아닙니다. 특정 사원에서 시작해 상위 관리자 방향으로 올라갈 수도 있습니다.

예를 들어 최개발부터 시작해서 대표까지 올라가 보겠습니다.

SELECT LEVEL,
       사원번호,
       사원명,
       직급,
       관리자번호
FROM 사원
START WITH 사원번호 = 'E004'
CONNECT BY 사원번호 = PRIOR 관리자번호;

이 조건은 다음 의미입니다.

자식 행의 관리자번호를 기준으로 부모 행의 사원번호를 찾는다.

결과적으로 최개발 → 이팀장 → 김대표 방향으로 상위 계층을 조회할 수 있습니다.


13. PRIOR 위치에 따른 방향 차이

PRIOR의 위치는 계층 탐색 방향을 이해하는 데 매우 중요합니다.

CONNECT BY 조건 의미 탐색 방향
CONNECT BY PRIOR 사원번호 = 관리자번호 부모 사원번호 = 자식 관리자번호 위에서 아래로
CONNECT BY 사원번호 = PRIOR 관리자번호 자식의 관리자번호를 따라 부모 사원번호 찾기 아래에서 위로

SQLD에서는 PRIOR가 어느 컬럼 앞에 붙어 있는지를 보고 부모와 자식의 연결 방향을 판단해야 합니다.


14. CONNECT_BY_ROOT

CONNECT_BY_ROOT는 현재 행이 속한 계층의 루트 값을 가져올 때 사용합니다.

SELECT CONNECT_BY_ROOT 사원명 AS 루트사원,
       LEVEL,
       사원명,
       직급
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호;

위 SQL은 각 행마다 해당 계층의 최상위 루트 사원명을 함께 보여줍니다.

하나의 조직도에서는 모든 행의 루트사원이 김대표로 표시될 수 있습니다.


15. SYS_CONNECT_BY_PATH

SYS_CONNECT_BY_PATH는 루트부터 현재 행까지의 경로를 문자열로 보여주는 함수입니다.

SELECT LEVEL,
       사원명,
       SYS_CONNECT_BY_PATH(사원명, ' > ') AS 경로
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호;

결과는 다음과 같은 형태로 나타날 수 있습니다.

> 김대표
> 김대표 > 이팀장
> 김대표 > 이팀장 > 최개발

메뉴 경로, 카테고리 경로, 조직도 경로를 보여줄 때 유용합니다.


16. CONNECT_BY_ISLEAF

CONNECT_BY_ISLEAF는 현재 행이 리프 노드인지 확인할 때 사용합니다.

CONNECT_BY_ISLEAF는 현재 행이 더 이상 자식이 없는 마지막 노드이면 1, 아니면 0을 반환한다.

SELECT LEVEL,
       사원명,
       직급,
       CONNECT_BY_ISLEAF AS 리프여부
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호;

백엔드개발자, 프론트엔드개발자, 서비스기획자처럼 하위 사원이 없는 데이터는 리프 노드가 될 수 있습니다.


17. ORDER SIBLINGS BY

ORDER SIBLINGS BY는 같은 부모를 가진 형제 노드끼리 정렬할 때 사용합니다.

ORDER SIBLINGS BY는 계층 구조를 유지하면서 같은 레벨의 형제 노드만 정렬하는 구문이다.

SELECT LEVEL,
       LPAD(' ', (LEVEL - 1) * 2) || 사원명 AS 조직도,
       직급
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호
ORDER SIBLINGS BY 사원명;

일반 ORDER BY를 사용하면 계층 구조가 깨질 수 있습니다. 하지만 ORDER SIBLINGS BY는 부모-자식 관계를 유지하면서 같은 부모 아래의 형제들만 정렬합니다.


18. ORDER BY와 ORDER SIBLINGS BY 차이

구분 ORDER BY ORDER SIBLINGS BY
정렬 대상 전체 결과 같은 부모를 가진 형제 노드
계층 구조 깨질 수 있음 유지됨
사용 상황 일반 정렬 계층형 질의 정렬

계층형 질의에서 정렬이 필요하면 ORDER SIBLINGS BY를 먼저 떠올리면 좋습니다.


19. WHERE절과 계층형 질의

계층형 질의에서도 WHERE절을 사용할 수 있습니다. 하지만 WHERE절은 계층 연결 조건이 아니라 최종 결과를 필터링하는 조건으로 이해해야 합니다.

SELECT LEVEL,
       사원번호,
       사원명,
       직급
FROM 사원
WHERE 직급 LIKE '%개발%'
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호;

위 SQL은 계층 구조를 만든 뒤, 직급에 개발이 포함된 행만 보여주는 방식으로 이해할 수 있습니다.

계층 연결 자체를 제한하고 싶다면 CONNECT BY 조건에 추가 조건을 넣는 방식을 고려해야 합니다.


20. CONNECT BY 조건 추가

CONNECT BY에는 부모-자식 연결 조건 외에 추가 조건을 넣을 수 있습니다.

SELECT LEVEL,
       사원번호,
       사원명,
       직급
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호
       AND 직급 <> '퇴사자';

이처럼 CONNECT BY 조건에 추가 조건을 넣으면 계층을 확장하는 과정에서 특정 데이터를 제외할 수 있습니다.


21. 순환 구조 문제

계층형 데이터에서 잘못된 데이터가 있으면 순환 구조가 생길 수 있습니다.

예를 들어 A의 관리자가 B이고, B의 관리자가 다시 A라면 서로가 서로를 참조하는 구조가 됩니다. 이런 경우 계층 탐색이 무한 반복될 수 있습니다.

A → B → A → B → ...

이런 문제를 방지하기 위해 Oracle에서는 NOCYCLE 옵션을 사용할 수 있습니다.


22. NOCYCLE

NOCYCLE은 계층형 질의에서 순환 구조가 발생해도 오류를 방지하고 조회를 계속할 수 있게 하는 옵션입니다.

SELECT LEVEL,
       사원번호,
       사원명,
       관리자번호
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY NOCYCLE PRIOR 사원번호 = 관리자번호;

NOCYCLE은 순환 구조가 있을 수 있는 데이터에서 안전하게 계층형 질의를 수행할 때 사용할 수 있습니다.


23. CONNECT_BY_ISCYCLE

CONNECT_BY_ISCYCLE은 현재 행이 순환 구조와 관련되어 있는지 확인할 때 사용합니다. 일반적으로 NOCYCLE과 함께 사용합니다.

SELECT LEVEL,
       사원번호,
       사원명,
       관리자번호,
       CONNECT_BY_ISCYCLE AS 순환여부
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY NOCYCLE PRIOR 사원번호 = 관리자번호;

순환이 감지된 행은 순환여부 값으로 확인할 수 있습니다. SQLD에서는 깊게 다루기보다, 순환 방지를 위해 NOCYCLE을 사용할 수 있다는 정도를 기억하면 좋습니다.


24. 계층형 질의 실행 흐름

계층형 질의는 학습 관점에서 다음 흐름으로 이해하면 좋습니다.

  1. FROM절에서 조회할 테이블을 정한다.
  2. START WITH로 루트 노드를 찾는다.
  3. CONNECT BY 조건으로 부모와 자식 행을 연결한다.
  4. LEVEL 등 계층 정보를 계산한다.
  5. 필요하면 WHERE로 결과를 필터링한다.
  6. ORDER SIBLINGS BY로 형제 노드를 정렬한다.

25. 계층형 질의 전체 예시

지금까지 배운 내용을 모두 포함한 예시입니다.

SELECT LEVEL,
       CONNECT_BY_ROOT 사원명 AS 루트사원,
       LPAD(' ', (LEVEL - 1) * 2) || 사원명 AS 조직도,
       직급,
       SYS_CONNECT_BY_PATH(사원명, ' > ') AS 경로,
       CONNECT_BY_ISLEAF AS 리프여부
FROM 사원
START WITH 관리자번호 IS NULL
CONNECT BY PRIOR 사원번호 = 관리자번호
ORDER SIBLINGS BY 사원명;

이 SQL은 다음 정보를 함께 보여줍니다.

  • LEVEL : 계층 깊이
  • CONNECT_BY_ROOT : 루트 사원
  • LPAD : 조직도 들여쓰기
  • SYS_CONNECT_BY_PATH : 루트부터 현재 행까지 경로
  • CONNECT_BY_ISLEAF : 리프 노드 여부
  • ORDER SIBLINGS BY : 형제 노드 정렬

26. 계층형 질의와 셀프 조인 비교

계층형 질의는 자기 자신을 참조하는 구조이므로 셀프 조인과 관련이 있습니다. 하지만 목적과 표현 방식이 다릅니다.

구분 셀프 조인 계층형 질의
목적 같은 테이블을 두 번 사용해 직접 관계 조회 상하 관계를 여러 단계로 탐색
대표 문법 JOIN START WITH, CONNECT BY PRIOR
적합한 경우 사원과 직속 관리자 조회 전체 조직도 조회
계층 깊이 고정된 관계에 적합 깊이가 여러 단계여도 조회 가능

직속 관리자만 보고 싶으면 셀프 조인이 적합할 수 있고, 대표부터 전체 하위 조직까지 보고 싶으면 계층형 질의가 적합합니다.


27. 계층형 질의에서 자주 하는 실수

실수 주의할 점
PRIOR 위치를 헷갈림 PRIOR가 붙은 컬럼은 부모 행의 컬럼이다.
START WITH를 생략하거나 잘못 지정함 루트 노드를 정확히 지정해야 한다.
일반 ORDER BY를 사용함 계층 구조 정렬은 ORDER SIBLINGS BY를 고려한다.
LEVEL이 0부터 시작한다고 생각함 LEVEL은 루트 노드가 1부터 시작한다.
순환 구조를 고려하지 않음 순환 가능성이 있으면 NOCYCLE을 고려한다.
WHERE와 CONNECT BY 조건의 역할을 혼동함 CONNECT BY는 연결 조건, WHERE는 결과 필터 조건이다.

28. SQLD 관점에서 꼭 기억할 내용

이번 글에서 SQLD 공부를 위해 꼭 기억해야 할 내용은 다음과 같습니다.

  • 계층형 질의는 부모-자식 관계를 가진 데이터를 트리 구조로 조회한다.
  • START WITH는 계층 탐색의 시작점, 즉 루트 노드를 지정한다.
  • CONNECT BY는 부모와 자식 행의 연결 조건을 지정한다.
  • PRIOR가 붙은 컬럼은 부모 행의 컬럼을 의미한다.
  • CONNECT BY PRIOR 사원번호 = 관리자번호는 위에서 아래로 탐색하는 대표 패턴이다.
  • LEVEL은 루트 노드를 1로 시작하는 계층 깊이이다.
  • CONNECT_BY_ROOT는 루트 값을 가져온다.
  • SYS_CONNECT_BY_PATH는 루트부터 현재 행까지의 경로를 보여준다.
  • CONNECT_BY_ISLEAF는 리프 노드 여부를 알려준다.
  • ORDER SIBLINGS BY는 계층 구조를 유지하면서 형제 노드를 정렬한다.
  • NOCYCLE은 순환 구조로 인한 오류를 방지할 때 사용한다.

29. 핵심 요약

개념 핵심 내용
계층형 질의 부모-자식 관계를 가진 데이터를 트리 구조로 조회하는 SQL
START WITH 계층 탐색을 시작할 루트 노드 지정
CONNECT BY 부모와 자식 행을 연결하는 조건 지정
PRIOR 부모 행의 컬럼을 의미
LEVEL 현재 행의 계층 깊이
CONNECT_BY_ROOT 현재 행이 속한 계층의 루트 값 반환
SYS_CONNECT_BY_PATH 루트부터 현재 행까지의 경로 반환
CONNECT_BY_ISLEAF 리프 노드 여부 반환
ORDER SIBLINGS BY 계층 구조를 유지하며 형제 노드 정렬
NOCYCLE 순환 구조 발생 시 오류 방지

30. 마무리

이번 글에서는 SQLD에서 빠뜨리기 쉬운 계층형 질의에 대해 정리했습니다.

계층형 질의는 조직도나 메뉴 구조처럼 부모와 자식 관계를 가진 데이터를 조회할 때 사용합니다. START WITH는 시작 지점을 정하고, CONNECT BY PRIOR는 부모와 자식의 연결 관계를 정의합니다.

특히 SQLD에서는 PRIOR의 위치를 보고 탐색 방향을 이해하는 것이 중요합니다. 또한 LEVEL, CONNECT_BY_ROOT, SYS_CONNECT_BY_PATH, CONNECT_BY_ISLEAF, ORDER SIBLINGS BY 같은 계층형 질의 관련 키워드도 함께 기억해두면 좋습니다.

다음 글에서는 SQLD 보강 주제로 PIVOT, UNPIVOT에 대해 정리해 보겠습니다.