2025-08-24 13:28
데이터베이스 고수의 비밀 무기 셀프 조인(Self Join) 완벽 정복 핸드북
데이터베이스를 다루는 여정에서 우리는 종종 여러 테이블을 연결하여 의미 있는 정보를 추출하는 JOIN
의 마법을 경험합니다. INNER JOIN
, LEFT JOIN
, FULL OUTER JOIN
등 다양한 JOIN 기법들은 흩어져 있는 데이터 퍼즐 조각을 맞추는 데 필수적입니다. 하지만 만약 우리가 찾아야 할 관계의 실마리가 여러 테이블이 아닌, 단 하나의 테이블 안에 있다면 어떻게 해야 할까요?
이 질문에 대한 해답이 바로 **셀프 조인(Self Join)**입니다. 셀프 조인은 이름 그대로 테이블이 ‘자기 자신’과 조인하는, 다소 생소하지만 매우 강력하고 실용적인 SQL 기법입니다. 이 핸드북을 통해 셀프 조인의 개념을 완벽하게 이해하고, 여러분의 데이터 분석 능력을 한 단계 끌어올려 보세요.
1. 셀프 조인은 왜 만들어졌을까? (탄생 배경)
셀프 조인의 필요성을 이해하기 위해 간단한 비유를 들어보겠습니다.
여러분이 한 회사의 모든 직원이 등록된
Employees
라는 테이블을 가지고 있다고 상상해 보세요. 이 테이블에는 각 직원의 ID, 이름, 그리고 그 직원을 관리하는 ‘매니저의 ID’가 포함되어 있습니다.이때, “각 직원의 이름과 그 직원의 매니저 이름을 함께 출력하라”는 요구사항이 들어왔습니다.
이 상황에서 우리는 난관에 부딪힙니다. 직원 정보와 매니저 정보 모두 Employees
테이블 안에 있기 때문입니다. 매니저 역시 이 회사의 직원이므로, 그의 이름도 같은 테이블에 저장되어 있습니다.
employee_id | employee_name | manager_id |
101 | 이서준 | 103 |
102 | 김하윤 | 103 |
103 | 박도윤 | 104 |
104 | 최지안 | NULL |
105 | 정시우 | 103 |
위 테이블에서 ‘이서준’의 매니저 ID는 ‘103’입니다. 우리는 ‘103’이라는 ID를 가지고 다시 이 테이블에서 employee_id
가 ‘103’인 직원을 찾아 ‘박도윤’이라는 이름을 알아내야 합니다.
이처럼 하나의 테이블 내에 계층적 관계나 상호 참조 관계가 포함된 데이터를 처리하기 위해 셀프 조인이 탄생했습니다. 셀프 조인이 없다면, 우리는 필요한 데이터를 여러 번 쿼리하거나 애플리케이션 레벨에서 복잡한 로직을 구현해야 했을 것입니다. 셀프 조인은 이러한 비효율을 SQL 쿼리 한 번으로 해결해주는 우아한 해결책입니다.
핵심: 셀프 조인은 하나의 테이블이 두 가지 이상의 역할을 동시에 수행할 때, 이 역할들 간의 관계를 정의하고 조회하기 위해 사용됩니다. (예: 직원과 매니저, 부모 카테고리와 자식 카테고리 등)
2. 셀프 조인의 구조와 원리
셀프 조인은 어떻게 테이블이 자기 자신과 조인하는 것을 가능하게 할까요? 그 비밀은 바로 **테이블 별칭(Alias)**에 있습니다.
SQL은 FROM
절에서 동일한 테이블을 여러 번 참조할 때, 각 참조에 대해 고유한 별칭을 부여하여 마치 서로 다른 테이블인 것처럼 인식하게 만듭니다.
FROM Employees AS e -- 첫 번째 역할: '직원(Employee)'
JOIN Employees AS m -- 두 번째 역할: '매니저(Manager)'
ON e.manager_id = m.employee_id;
위 구문을 통해 데이터베이스는 Employees
테이블을 두 개의 독립적인 가상 테이블, 즉 ‘직원’ 역할을 하는 e
와 ‘매니저’ 역할을 하는 m
으로 취급합니다.
-
가상 테이블 생성: 데이터베이스는 메모리상에
Employees
테이블의 복사본 두 개를 만듭니다. 하나는e
라고 부르고, 다른 하나는m
이라고 부릅니다. -
관계 정의 (ON 절):
ON e.manager_id = m.employee_id
조건은 이 두 가상 테이블을 어떻게 연결할지 정의합니다.-
e
테이블(직원 테이블)의 한 행을 선택합니다. (예: ‘이서준’,manager_id
= 103) -
m
테이블(매니저 테이블)에서employee_id
가e
테이블의manager_id
와 같은 행을 찾습니다. (예:employee_id
= 103인 ‘박도윤’) -
두 행이 조건에 부합하면, 이 두 행의 정보를 결합하여 결과셋에 포함시킵니다.
-
이 과정을 모든 행에 대해 반복하면, 우리는 각 직원과 그에 해당하는 매니저 정보를 한 줄에 나란히 놓인 결과로 얻을 수 있습니다.
결론적으로 셀프 조인은 새로운 문법이 아니라, 기존의 JOIN
문법에 별칭(Alias)
을 활용하여 동일한 테이블을 마치 다른 테이블처럼 다루는 ‘기법’ 또는 ‘패턴’입니다.
3. 셀프 조인 실전 사용법 (예제 중심)
이제 실제 시나리오를 통해 셀프 조인을 어떻게 활용하는지 자세히 살펴보겠습니다.
예제 1: 모든 직원의 매니저 찾기 (가장 대표적인 예)
앞서 살펴본 Employees
테이블을 사용하여 각 직원의 이름과 그들의 매니저 이름을 조회해 보겠습니다.
테이블: Employees
employee_id | employee_name | manager_id |
101 | 이서준 | 103 |
102 | 김하윤 | 103 |
103 | 박도윤 | 104 |
104 | 최지안 | NULL |
105 | 정시우 | 103 |
쿼리:
SELECT
e.employee_name AS "직원 이름",
m.employee_name AS "매니저 이름"
FROM
Employees AS e
JOIN
Employees AS m ON e.manager_id = m.employee_id;
쿼리 해설:
-
SELECT e.employee_name, m.employee_name
: ‘직원’ 역할을 하는e
테이블에서는employee_name
을, ‘매니저’ 역할을 하는m
테이블에서도employee_name
을 선택합니다. 명확한 구분을 위해AS
를 사용하여 컬럼 별칭을 지정했습니다. -
FROM Employees AS e
:Employees
테이블을e
라는 별칭으로 지정하여 ‘직원’의 역할을 부여합니다. -
JOIN Employees AS m
:Employees
테이블을 다시 한번 가져와m
이라는 별칭으로 지정하고 ‘매니저’의 역할을 부여합니다. -
ON e.manager_id = m.employee_id
: ‘직원’의 매니저 ID와 ‘매니저’의 직원 ID가 같은 경우에 두 테이블을 연결하라고 명시합니다.
결과:
직원 이름 | 매니저 이름 |
이서준 | 박도윤 |
김하윤 | 박도윤 |
박도윤 | 최지안 |
정시우 | 박도윤 |
INNER JOIN
을 사용했기 때문에 manager_id
가 NULL
인 ‘최지안’은 결과에서 제외되었습니다. 만약 매니저가 없는 직원까지 포함하고 싶다면 어떻게 해야 할까요? 바로 이럴 때 LEFT JOIN
을 활용합니다.
SELECT
e.employee_name AS "직원 이름",
COALESCE(m.employee_name, '없음') AS "매니저 이름" -- NULL일 경우 '없음'으로 표시
FROM
Employees AS e
LEFT JOIN
Employees AS m ON e.manager_id = m.employee_id;
결과 (LEFT JOIN
사용 시):
직원 이름 | 매니저 이름 |
이서준 | 박도윤 |
김하윤 | 박도윤 |
박도윤 | 최지안 |
최지안 | 없음 |
정시우 | 박도윤 |
예제 2: 같은 도시에 거주하는 고객 쌍 찾기
이번에는 같은 테이블 내에서 특정 조건을 만족하는 데이터들의 ‘쌍(pair)‘을 찾아보겠습니다.
테이블: Customers
customer_id | customer_name | city |
1 | 홍길동 | 서울 |
2 | 이순신 | 부산 |
3 | 강감찬 | 서울 |
4 | 유관순 | 인천 |
5 | 세종대왕 | 서울 |
요구사항: 같은 도시에 사는 고객들을 짝지어 목록으로 만드세요.
쿼리:
SELECT
c1.customer_name,
c2.customer_name,
c1.city
FROM
Customers AS c1
JOIN
Customers AS c2 ON c1.city = c2.city AND c1.customer_id < c2.customer_id;
쿼리 해설:
-
ON c1.city = c2.city
: 두 가상 테이블c1
과c2
를city
컬럼을 기준으로 조인합니다. -
AND c1.customer_id < c2.customer_id
: 이 조건이 이 쿼리의 핵심입니다. 이 조건이 없다면 다음과 같은 문제가 발생합니다.-
자기 자신과의 페어링: (‘홍길동’, ‘홍길동’)과 같이 자기 자신과 짝지어지는 결과가 나옵니다.
-
중복 페어링: (‘홍길동’, ‘강감찬’)과 (‘강감찬’, ‘홍길동’)이 모두 결과에 포함됩니다.
c1.customer_id < c2.customer_id 조건을 추가함으로써, ID가 더 작은 고객이 항상 왼쪽에 오도록 하여 위 두 가지 문제를 한 번에 해결할 수 있습니다.
-
결과:
customer_name | customer_name | city |
홍길동 | 강감찬 | 서울 |
홍길동 | 세종대왕 | 서울 |
강감찬 | 세종대왕 | 서울 |
예제 3: 연속된 날짜의 데이터 비교하기
로그 데이터나 시계열 데이터를 분석할 때, 이전 행과 다음 행의 값을 비교해야 하는 경우가 많습니다. 셀프 조인은 이럴 때도 유용하게 사용될 수 있습니다.
테이블: DailySales
sale_date | amount |
2023-10-01 | 150 |
2023-10-02 | 180 |
2023-10-03 | 170 |
2023-10-04 | 200 |
요구사항: 각 날짜의 매출과 바로 전날의 매출을 함께 보여주세요.
쿼리:
SELECT
today.sale_date AS "오늘 날짜",
today.amount AS "오늘 매출",
yesterday.amount AS "어제 매출"
FROM
DailySales AS today
LEFT JOIN
DailySales AS yesterday ON today.sale_date = DATE_ADD(yesterday.sale_date, INTERVAL 1 DAY);
-- 참고: 사용하는 DBMS에 따라 날짜 함수는 달라질 수 있습니다.
-- PostgreSQL: ON today.sale_date = yesterday.sale_date + INTERVAL '1 day'
-- Oracle: ON today.sale_date = yesterday.sale_date + 1
쿼리 해설:
-
ON today.sale_date = DATE_ADD(yesterday.sale_date, INTERVAL 1 DAY)
: ‘오늘’ 테이블의 날짜가 ‘어제’ 테이블의 날짜에 하루를 더한 것과 같을 때 조인합니다. 즉,today
의 행과yesterday
의 바로 다음 날 행을 연결합니다. -
LEFT JOIN
을 사용하여 첫째 날(어제 데이터가 없는 날)의 데이터도 결과에 포함시킵니다.
결과:
오늘 날짜 | 오늘 매출 | 어제 매출 |
2023-10-01 | 150 | NULL |
2023-10-02 | 180 | 150 |
2023-10-03 | 170 | 180 |
2023-10-04 | 200 | 170 |
참고: 최신 DBMS에서는 LAG()
, LEAD()
와 같은 윈도우 함수(Window Function)를 사용하여 더 효율적으로 이러한 문제를 해결할 수 있습니다. 하지만 윈도우 함수를 지원하지 않는 환경이나, 더 복잡한 조건의 비교가 필요할 때는 여전히 셀프 조인이 강력한 대안이 됩니다.
4. 심화 내용 및 주의사항
성능 고려사항
셀프 조인은 매우 유용하지만, 대용량 테이블에서 사용할 때는 성능에 주의해야 합니다. 데이터베이스는 내부적으로 테이블을 두 번 읽어야 하므로, 일반적인 조인보다 더 많은 리소스를 사용할 수 있습니다.
성능 최적화를 위해 조인 조건에 사용되는 컬럼(예: manager_id
, employee_id
)에는 반드시 인덱스(Index)를 생성하는 것이 좋습니다. 인덱스는 데이터베이스가 특정 값을 빠르게 찾을 수 있도록 도와주는 목차와 같아서, 조인 성능을 극적으로 향상시킬 수 있습니다.
흔히 저지르는 실수
-
별칭(Alias)을 잊는 것: 셀프 조인에서 별칭은 선택이 아닌 필수입니다. 별칭을 사용하지 않으면 데이터베이스는 어떤 테이블의 컬럼을 참조하는지 구분할 수 없어 ‘ambiguous column name’(모호한 컬럼 이름) 오류를 발생시킵니다.
-
잘못된 조인 조건:
ON
절의 조건을 잘못 설정하면 의도치 않은 결과(카테시안 곱 등)가 발생하거나 원하는 결과를 얻지 못할 수 있습니다. 논리적 관계를 명확히 이해하고 조건을 작성해야 합니다. -
페어링 문제 미처리: 예제 2와 같이 쌍을 찾는 쿼리에서 자기 자신과의 페어링, 중복 페어링을 제거하는 조건을 누락하는 경우가 많습니다.
id_a < id_b
와 같은 트릭을 항상 기억하세요.
5. 결론: 셀프 조인, 언제 사용해야 할까?
지금까지 셀프 조인의 모든 것을 살펴보았습니다. 이제 여러분은 셀프 조인이 단순한 기술이 아니라, 데이터를 바라보는 새로운 관점을 제공하는 강력한 도구임을 이해하셨을 겁니다.
마지막으로 셀프 조인이 필요한 순간을 정리해 드립니다.
-
테이블 내에 계층 구조가 있을 때: 직원-매니저, 부모-자식 카테고리, 추천인-피추천인 관계 등
-
테이블 내의 항목들을 서로 비교해야 할 때: 같은 도시에 사는 고객, 같은 상품을 구매한 사용자, 특정 기간 내에 연속으로 발생한 이벤트 등
-
순차적인 데이터를 비교 분석해야 할 때: 전일 대비 매출 비교, 이전 로그와 현재 로그 비교 등
셀프 조인은 처음에는 개념적으로 낯설게 느껴질 수 있지만, 몇 번의 연습을 통해 익숙해지면 여러분의 SQL 쿼리 작성 능력을 한 차원 높여줄 것입니다. 테이블의 숨겨진 관계를 찾아내는 데이터 탐정처럼, 셀프 조인을 자유자재로 활용하여 더 깊이 있는 인사이트를 발견해 보세요.