본문 바로가기
SQL Professional

[독학! SQLP] 계층 쿼리

by 개발자의 2025. 8. 20.

계층형 쿼리?

  • 정의: 트리(tree) 구조처럼 상위-하위 관계(부모-자식)를 가지는 데이터를 조회하는 SQL 기법
  • 대표 문법 (Oracle)
SELECT ...
FROM table
START WITH 조건     -- 루트(최상위) 지정
CONNECT BY [NOCYCLE] PRIOR 부모 = 자식  -- 상위-하위 관계
ORDER SIBLINGS BY 컬럼  -- 형제 노드 간 정렬

예제 데이터 셋

-- EMP(사번, 이름, 상사사번, 부서)
CREATE TABLE EMP (
  EMPNO NUMBER PRIMARY KEY,
  ENAME VARCHAR2(20),
  MGR   NUMBER NULL,
  DEPT  VARCHAR2(10)
);

INSERT INTO EMP VALUES (100, 'KING' , NULL,   'HQ');     -- 루트
INSERT INTO EMP VALUES (110, 'JONES', 100 ,   'R&D');
INSERT INTO EMP VALUES (120, 'BLAKE', 100 ,   'SALES');
INSERT INTO EMP VALUES (130, 'CLARK', 100 ,   'FIN');
INSERT INTO EMP VALUES (201, 'ALLEN', 120 ,   'SALES');
INSERT INTO EMP VALUES (202, 'WARD' , 120 ,   'SALES');
INSERT INTO EMP VALUES (211, 'SMITH', 110 ,   'R&D');
INSERT INTO EMP VALUES (212, 'FORD' , 110 ,   'R&D');
INSERT INTO EMP VALUES (301, 'MILLER',130 ,   'FIN');
COMMIT;

주요 키워드로 알아보는 계층형 쿼리

START WITH

  • 계층형 쿼리의 시작점(루트 노드) 지정.
  • 조건을 만족하는 행부터 탐색을 시작함.
  • 지정하지 않으면 전체 테이블에서 탐색 시도 → 불필요한 결과나 에러 가능.
    • 모든 노드가 루트처럼 취급되어 다중 트리 발생 → 의도치 않은 중복.
  • 예시
-- 최고 관리자(KING)에서 시작
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
START WITH MGR IS NULL        -- 루트 조건
CONNECT BY PRIOR EMPNO = MGR; -- 부모 → 자식

-- JONES(110)을 루트로 시작
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
START WITH EMPNO = 110
CONNECT BY PRIOR EMPNO = MGR;

-- JONES(110), BLAKE(120) 둘 다 루트로 시작
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
START WITH EMPNO IN (110, 120)
CONNECT BY PRIOR EMPNO = MGR;

-- 루트 없이 실행할 경우.
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
CONNECT BY PRIOR EMPNO = MGR;

CONNECT BY

  • 상/하위 관계(재귀적 탐색)를 정의하는 구문.
  • PRIOR 키워드와 함꼐 부모 - 자식 관계를 지정해야 함.
  • 루트(START WITH)에서 시작해 CONNECT BY조건을 만족하는 행들을 계속 탐색
  • 예시
-- 부모(empno) → 자식(mgr)
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
START WITH MGR IS NULL                -- KING에서 시작
CONNECT BY PRIOR EMPNO = MGR;         -- 부모 → 자식 관계


-- CONNECT BY 안에 추가 조건을 넣어 특정 경로만 탐색
-- R&D 부서만 추적
SELECT LEVEL, EMPNO, ENAME, DEPT
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
       AND DEPT = 'R&D';

→ CONNECT BY PRIOR EMPNO = MGR 조건이 “부모(empno) → 자식(mgr)” 관계를 만든 것.

PRIOR

  • CONNECT BY에서 부모와 자식중 어느쪽을 기준으로 연결할지 지정
  • PRIOR가 붙은 쪽이 부모!
  • 왼쪽 PRIOR = 부모 → 자식
  • 오른쪽 PRIOR = 자식 → 부모
  • PRIOR를 양쪽에 둘 다 쓰는 건 불가. 반드시 한쪽에만!
  • 예시
  •  
-- 부모 → 자식 (하향식 탐색)
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR;   -- 부모(empno) → 자식(mgr)

-- 자식 → 부모 (상향식 탐색)
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
START WITH EMPNO = 201          -- ALLEN(부하)에서 시작
CONNECT BY EMPNO = PRIOR MGR;   -- 자식(empno) → 부모(mgr

LEVEL 

  • 계층 깊이 반환 (루트는 1, 자식은 2, 손자는 3 ...)
  • 루트(START WITH 지정)는 항상 1부터 시작.
  • 예시
SELECT LEVEL, EMPNO, ENAME, MGR
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
ORDER SIBLINGS BY ENAME;

-- 들여쓰기
SELECT LPAD(' ', (LEVEL-1)*2) || ENAME AS TREE, EMPNO, MGR
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
ORDER SIBLINGS BY ENAME;

-- 특정 깊이만 조회
-- 2단계(부하 직원)까지만 조회
SELECT LEVEL, ENAME
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
AND LEVEL <= 2;

-- 조건식에 확용
-- 말단 직원만 출력 (LEVEL 이용)
SELECT LEVEL, ENAME
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
HAVING LEVEL = MAX(LEVEL);  -- 집계랑 같이 쓰는 유형도 출제

ORDER SIBLINGS BY

  • 계층형 쿼리에서 같은 부모(=형제 노드) 아래에 있는 행들만 정렬.
  • 일반 ORDER BY를 쓰면 계층 구조가 깨짐 → 시험 함정 포인트.
  • 따라서 트리 구조 유지 + 형제 간 정렬이 필요할 때 반드시 ORDER SIBLINGS BY 사용.
  • 예시
SELECT LPAD(' ', (LEVEL-1)*2) || ENAME AS TREE,
       EMPNO, MGR
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
ORDER SIBLINGS BY ENAME;

-- 일반 order by 와 ORDER SIBLINGS BY 비교

-- 일반 order by
-- 결과: 전체 데이터가 이름 알파벳순으로 섞여서 계층 구조 붕괴.
SELECT LEVEL, ENAME, MGR
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
ORDER BY ENAME;    -- 일반 ORDER BY

-- ORDER SIBLINGS BY
-- 결과: 계층 유지 + 형제 간 정렬만 적용.
SELECT LEVEL, ENAME, MGR
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
ORDER SIBLINGS BY ENAME;

NOCYCLE & CONNECT_BY_ISCYCLE

  • 순환(Cycle) : 계층 구조에서 부모-자식 참조가 자기 자신에게 다시 돌아오는 경우. → 무한 루프 발생 위험.
  • NOCYCLE : 순환 발견 시 해당 경로를 끊고 탐색 중단.
  • CONNECT_BY_ISCYCLE : 현재 행이 순환에 포함되었는지 여부 반환 (1=순환, 0=아님).
  • 예시
-- 순환 포함 예시 데이터

-- WARDCYCLE(333)이 BLAKE(120)의 상사,
-- BLAKE(120)가 다시 WARDCYCLE(333)의 상사 → 순환 발생
UPDATE EMP SET MGR = 333 WHERE EMPNO = 120;  -- BLAKE → WARDCYCLE
-- 이미 EMP(333, 'WARDCYCLE', 120, 'SALES') 있음 (WARDCYCLE → BLAKE)
COMMIT;

-- 순환 방지 옵션(NOCYCLE) 없이 실행
-- 결과 : ORA-01436 오류 발생 (“CONNECT BY loop in user data” → 순환 구조 탐지됨)
SELECT ENAME, LEVEL
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR;

-- NOCYCLE 옵션 사용
-- 결과: 순환되는 경로(BLAKE ↔ WARDCYCLE)에서 경로가 끊기고 탐색 중단, 나머지 정상 트리는 계속 조회됨.
SELECT ENAME, LEVEL, SYS_CONNECT_BY_PATH(ENAME, '->') AS PATH
FROM   EMP
START WITH MGR IS NULL
CONNECT BY NOCYCLE PRIOR EMPNO = MGR;

-- CONNECT_BY_ISCYCLE 옵션 사용 (순환에 걸린 행 식별용)
-- WARDCYCLE 행에서 IS_CYCLE=1로 표시
SELECT ENAME,
       CONNECT_BY_ISCYCLE AS IS_CYCLE,
       SYS_CONNECT_BY_PATH(ENAME, '->') AS PATH
FROM   EMP
START WITH MGR IS NULL
CONNECT BY NOCYCLE PRIOR EMPNO = MGR;

CONNECT_BY_ISLEAF

  • 계층형 쿼리에서 현재 행이 자식(하위 노드)을 가지고 있는지 여부를 반환.
  • 1 = 리프(leaf, 말단 노드) → 더 이상 하위 없음
  • 0 = 내부 노드 → 하위 노드 존재
  • 예시
SELECT ENAME,
       CONNECT_BY_ISLEAF AS IS_LEAF,
       LEVEL
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
ORDER SIBLINGS BY ENAME;

-- 말단 직원만 표시
SELECT ENAME, LEVEL
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
WHERE CONNECT_BY_ISLEAF = 1;

-- 트리경로 + 말단 표시
SELECT ENAME,
       SYS_CONNECT_BY_PATH(ENAME, '->') AS PATH,
       CONNECT_BY_ISLEAF AS IS_LEAF
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR;

SYS_CONNECT_BY_PATH

  • 계층형 쿼리에서 루트 → 현재 노드까지의 경로를 문자열로 반환.
  • 구분자를 지정할 수 있음.
  • 보통 ->, /, : 같은 기호를 많이 씀.
  • 예시
SELECT ENAME,
       SYS_CONNECT_BY_PATH(ENAME, '->') AS PATH,
       LEVEL
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
ORDER SIBLINGS BY ENAME;

-- 리프노드 최종 경로만 출력
SELECT SYS_CONNECT_BY_PATH(ENAME, '->') AS FULL_PATH
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
WHERE CONNECT_BY_ISLEAF = 1;

-- 경로에서 루트 제거
SELECT ENAME,
       SUBSTR(SYS_CONNECT_BY_PATH(ENAME, '/'), 2) AS PATH
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR;

-- 조건 필터와 조합
-- 특정 사원까지의 경로 조회
SELECT SYS_CONNECT_BY_PATH(ENAME, '->') AS PATH
FROM   EMP
START WITH MGR IS NULL
CONNECT BY PRIOR EMPNO = MGR
AND ENAME = 'SMITH';

계층형 쿼리(Oracle) vs 재귀 CTE(ANSI)

  • START WITH ↔ 앵커 SELECT
  • CONNECT BY PRIOR ↔ 재귀 JOIN
  • LEVEL ↔ 누적 +1 컬럼
  • 예시
-- ANSI (예: PostgreSQL/MariaDB 10.2+)
WITH RECURSIVE EMP_HIER AS (
  -- 앵커: 루트
  SELECT EMPNO, ENAME, MGR, 1 AS LVL, CAST(ENAME AS VARCHAR(4000)) AS PATH
  FROM   EMP
  WHERE  MGR IS NULL
  UNION ALL
  -- 재귀: 자식 확장
  SELECT E.EMPNO, E.ENAME, E.MGR, H.LVL + 1,
         CONCAT(H.PATH, '->', E.ENAME)
  FROM   EMP E
  JOIN   EMP_HIER H ON E.MGR = H.EMPNO
  -- (순환 방지는 보통 방문체크용 키 누적 후 NOT LIKE/NOT IN 등으로 구현)
)
SELECT * FROM EMP_HIER
ORDER BY PATH;

'SQL Professional' 카테고리의 다른 글

[독학! SQLP] SQLP 낙방 후기  (1) 2025.08.25
[독학! SQLP] 데이터베이스 구조  (4) 2025.08.21
[독학! SQLP] 서브쿼리  (3) 2025.08.20
[독학! SQLP] 트랜잭션  (1) 2025.08.20
[독학! SQLP] SQL 조인!  (6) 2025.08.20