이번 문서의 목표: 이 파일을 다 읽으면 두 테이블을 주고 조인 종류만 알려줬을 때 결과 행이 몇 개, 어떤 값으로 나오는지 손으로 직접 예측할 수 있다.
조인이 왜 필요한가
관계형 데이터베이스(Relational Database)는 08편에서 다뤘듯, 하나의 큰 표에 모든 정보를 다 넣지 않고 엔터티(entity, 관리 대상이 되는 사물이나 개념)별로 여러 테이블에 나눠 저장한다. 예를 들어 “사원이 어느 부서에 속하는가”라는 정보는 사원 테이블과 부서 테이블 두 곳에 걸쳐 있다. 사원 테이블은 자신이 속한 부서의 번호(외래키, foreign key — 다른 테이블의 기본키를 참조하는 컬럼)만 들고 있을 뿐, 그 부서의 이름은 부서 테이블에만 있다.
그런데 “사원 이름과 그 사원이 속한 부서 이름을 나란히 보여줘”라는 질문에 답하려면, 결국 두 테이블을 어떤 기준으로 이어 붙여야 한다. 이렇게 여러 테이블을 공통된 컬럼(주로 기본키와 외래키) 값을 기준으로 옆으로 이어 붙여 하나의 결과 집합으로 만드는 연산이 조인(JOIN)이다.
쉽게 말하면: 조인은 “이 값이 같은 행끼리 옆으로 붙여라”는 명령이다.
조인의 원리 — 실습용 테이블 두 개
앞으로 이 편과 다음 두 편(17. 서브쿼리, 18. 집합 연산자)에서 계속 재사용할 두 테이블을 정한다. 작은 테이블이지만 NULL이 있는 행과 상대 테이블에 짝이 없는 행을 일부러 넣어뒀다. 실제 기출에서 조인 결과 건수를 틀리게 만드는 함정이 바로 이 두 가지이기 때문이다.
DEPT(부서) 테이블 — 기본키는 DEPT_ID
| DEPT_ID | DNAME |
|---|---|
| 10 | 영업부 |
| 20 | 개발부 |
| 30 | 인사부 |
| 40 | 마케팅부 |
EMP(사원) 테이블 — 기본키는 EMP_ID, DEPT_ID는 DEPT를 가리키는 외래키, MGR_ID는 자신의 관리자 사원 번호(자기 자신과 같은 EMP 테이블의 EMP_ID를 가리킨다)
| EMP_ID | NAME | DEPT_ID | MGR_ID |
|---|---|---|---|
| 1 | 김민준 | 10 | (NULL) |
| 2 | 이서연 | 20 | 1 |
| 3 | 박도윤 | 20 | 1 |
| 4 | 최지우 | (NULL) | 2 |
| 5 | 정하은 | 50 | 2 |
일부러 넣은 함정 세 가지를 미리 짚어둔다.
- 최지우의 DEPT_ID가 NULL이다. 아직 부서 배치를 받지 못한 신입사원이라고 생각하면 된다.
- 정하은의 DEPT_ID가 50이다. 그런데 DEPT 테이블에는 DEPT_ID가 50인 부서가 없다. 오타이거나 삭제된 부서라고 생각하면 된다.
- 인사부(30)와 마케팅부(40)에는 소속 사원이 한 명도 없다. DEPT 테이블에는 있지만 EMP 테이블 어디에도 DEPT_ID 30이나 40이 나오지 않는다.
이 세 가지 함정을 기억해두고, 아래에서 같은 EMP·DEPT 데이터로 조인 종류를 바꿔가며 결과가 어떻게 달라지는지 전부 표로 확인한다.
INNER JOIN — 양쪽에 다 있는 것만
INNER JOIN은 조인 조건(주로 EMP.DEPT_ID = DEPT.DEPT_ID처럼 등호로 비교하는 등가 조인 조건)을 만족하는 행끼리만 결합해서 보여준다. 어느 한쪽이라도 짝이 없으면 그 행은 결과에서 통째로 빠진다.
SELECT E.NAME, D.DNAME
FROM EMP E
INNER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;| NAME | DNAME |
|---|---|
| 김민준 | 영업부 |
| 이서연 | 개발부 |
| 박도윤 | 개발부 |
결과가 3행인 이유를 하나씩 세어본다. EMP는 5행이지만, 최지우는 DEPT_ID가 NULL이라 애초에 비교 대상 자체가 없어 탈락하고, 정하은은 DEPT_ID가 50인데 DEPT 테이블에 50이 없어 탈락한다. 남은 김민준(10→영업부), 이서연(20→개발부), 박도윤(20→개발부) 세 명만 짝이 맞아 결과에 남는다. 인사부와 마케팅부는 애초에 EMP 쪽에 DEPT_ID가 일치하는 행이 없으므로 등장하지 않는다.
NULL은 조인 조건에서 절대 매칭되지 않는다
최지우의 사례가 바로 이 규칙을 보여준다. NULL = 20도, NULL = NULL조차도 SQL에서는 참(TRUE)이 아니라 알 수 없음(UNKNOWN)으로 평가된다. 09편에서 다뤘듯 NULL은 “값이 없다”는 상태이지 비교 가능한 값이 아니기 때문이다. 그래서 조인 조건에 쓰인 컬럼이 NULL이면, 그 값이 상대편의 어떤 값과도 절대 같아질 수 없고, INNER JOIN에서는 무조건 탈락한다. “NULL은 NULL끼리도 매칭되지 않는다”는 점을 반드시 기억한다.
LEFT OUTER JOIN — 왼쪽 테이블은 무조건 다 살린다
LEFT OUTER JOIN(줄여서 LEFT JOIN)은 INNER JOIN 결과에 더해, 왼쪽(FROM 절에 먼저 쓴) 테이블의 행은 짝을 못 찾아도 버리지 않고 남긴다. 이때 오른쪽 테이블에서 가져올 컬럼 자리는 NULL로 채운다.
SELECT E.NAME, D.DNAME
FROM EMP E
LEFT OUTER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;| NAME | DNAME |
|---|---|
| 김민준 | 영업부 |
| 이서연 | 개발부 |
| 박도윤 | 개발부 |
| 최지우 | (NULL) |
| 정하은 | (NULL) |
5행이 나오는 이유. EMP가 왼쪽 테이블이므로 EMP의 5행이 전부 보존된다. 그중 3행은 INNER JOIN과 똑같이 정상적으로 짝을 찾고, 짝을 못 찾은 최지우와 정하은은 DNAME 자리가 NULL인 채로 결과에 남는다. 왼쪽 테이블의 행 수만큼은 최소한 결과에 나온다는 점이 LEFT OUTER JOIN의 핵심이다(단, 오른쪽에서 한 행에 여러 개가 매칭되면 그만큼 늘어날 수도 있다).
RIGHT OUTER JOIN — 이번엔 오른쪽 테이블을 무조건 다 살린다
RIGHT OUTER JOIN은 방향만 반대다. 오른쪽 테이블의 행은 짝을 못 찾아도 전부 남기고, 왼쪽에서 가져올 자리를 NULL로 채운다.
SELECT E.NAME, D.DNAME
FROM EMP E
RIGHT OUTER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;| NAME | DNAME |
|---|---|
| 김민준 | 영업부 |
| 이서연 | 개발부 |
| 박도윤 | 개발부 |
| (NULL) | 인사부 |
| (NULL) | 마케팅부 |
5행이 나오는 이유. 이번엔 DEPT가 오른쪽 테이블이므로 DEPT의 4행(영업부, 개발부, 인사부, 마케팅부)이 전부 보존 대상이다. 영업부와 개발부는 사원이 있어 정상 매칭되고(개발부는 이서연·박도윤 2명과 매칭되어 2행이 된다), 인사부와 마케팅부는 소속 사원이 아예 없으므로 NAME 자리가 NULL인 채로 한 행씩 등장한다. 정하은의 DEPT_ID인 50은 DEPT 테이블에 없는 값이므로, DEPT를 기준으로 살리는 RIGHT OUTER JOIN에서는 애초에 등장할 자리가 없다는 점도 확인해두자.
FULL OUTER JOIN — 양쪽 다 무조건 살린다
FULL OUTER JOIN은 LEFT와 RIGHT를 합친 것이다. 양쪽 테이블의 행을 전부 보존하며, 짝이 있으면 결합하고 없으면 그 자리를 NULL로 채운다.
SELECT E.NAME, D.DNAME
FROM EMP E
FULL OUTER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;| NAME | DNAME |
|---|---|
| 김민준 | 영업부 |
| 이서연 | 개발부 |
| 박도윤 | 개발부 |
| 최지우 | (NULL) |
| 정하은 | (NULL) |
| (NULL) | 인사부 |
| (NULL) | 마케팅부 |
7행이 나오는 이유를 세 부분으로 나눠 더한다. ① 양쪽 다 짝을 찾은 매칭 행 3개(김민준, 이서연, 박도윤). ② EMP에만 있고 DEPT에는 짝이 없는 행 2개(최지우, 정하은) — LEFT OUTER JOIN에서 남았던 바로 그 행들이다. ③ DEPT에만 있고 EMP에는 짝이 없는 행 2개(인사부, 마케팅부) — RIGHT OUTER JOIN에서 남았던 바로 그 행들이다. 3 + 2 + 2 = 7행. FULL OUTER JOIN 결과 건수는 INNER JOIN 매칭 건수에 양쪽 미매칭 건수를 모두 더한 값이라는 공식으로 기억해두면, 실행 결과 예측 문항에서 검산하기 좋다.
일부 DBMS(대표적으로 MySQL)는 FULL OUTER JOIN 문법을 직접 지원하지 않는다. 이 경우 LEFT OUTER JOIN 결과와 RIGHT OUTER JOIN 결과를 UNION으로 합쳐 흉내 낸다(집합 연산자 UNION은 18편에서 다룬다).
CROSS JOIN — 조인 조건 없이 모든 조합
CROSS JOIN은 조인 조건 자체가 없다. 왼쪽 테이블의 모든 행과 오른쪽 테이블의 모든 행을 하나도 빠짐없이 다 조합한다. 수학의 곱집합(Cartesian Product, 카티전 곱)과 같은 개념이다.
SELECT E.NAME, D.DNAME
FROM EMP E
CROSS JOIN DEPT D;EMP가 5행, DEPT가 4행이므로 결과는 5 곱하기 4, 즉 20행이다. 김민준은 영업부·개발부·인사부·마케팅부 4개 부서 모두와 한 번씩 짝지어지고, 나머지 사원 4명도 각각 4개 부서와 전부 조합된다. 조인 조건이 없으므로 NULL이든 존재하지 않는 DEPT_ID든 전혀 문제가 되지 않는다 — 애초에 조건 자체를 비교하지 않기 때문이다. FROM EMP, DEPT처럼 콤마로 두 테이블을 나열하고 WHERE 절에 조인 조건을 아예 안 붙인 경우도 결과적으로 CROSS JOIN과 같아진다는 점을 기출에서 자주 확인하니 기억해두자.
SELF JOIN — 자기 자신과 조인한다
SELF JOIN은 새로운 문법이 아니라, 같은 테이블을 서로 다른 별칭(alias) 두 개로 두 번 등장시켜 조인하는 것이다. “사원과 그 사원의 관리자”처럼, 한 테이블 안에 계층 관계(사원 번호와 관리자 번호가 같은 테이블의 같은 컬럼을 가리키는 관계)가 있을 때 쓴다.
SELECT E.NAME AS 사원, M.NAME AS 관리자
FROM EMP E
JOIN EMP M ON E.MGR_ID = M.EMP_ID;| 사원 | 관리자 |
|---|---|
| 이서연 | 김민준 |
| 박도윤 | 김민준 |
| 최지우 | 이서연 |
| 정하은 | 이서연 |
EMP를 사원 역할(E)과 관리자 역할(M)로 두 번 참조했을 뿐, 실제로는 하나의 물리적 테이블이다. 김민준은 MGR_ID가 NULL(최상위 관리자라 자신의 위 상사가 없다는 뜻)이므로 INNER JOIN 조건에서 짝을 찾지 못해 “사원” 자리에는 등장하지 않는다(관리자 자리로는 등장한다). SELF JOIN도 결국 INNER JOIN이나 OUTER JOIN 중 하나로 동작하므로, 여기서도 NULL 미매칭 규칙이 똑같이 적용된다.
USING과 NATURAL JOIN — ON을 줄여 쓰는 두 가지 방법
두 테이블의 조인 기준 컬럼 이름이 같을 때, ON 대신 더 짧게 쓸 수 있는 문법이 두 가지 있다.
USING은 이름이 같은 컬럼 중에서 직접 지정한 컬럼만 조인 기준으로 쓴다.
SELECT E.NAME, D.DNAME
FROM EMP E JOIN DEPT D USING (DEPT_ID);이 결과는 ON E.DEPT_ID = D.DEPT_ID를 쓴 INNER JOIN과 완전히 동일하다.
NATURAL JOIN은 지정 없이 두 테이블에서 이름이 같은 컬럼을 전부 자동으로 찾아 조인 조건으로 쓴다.
SELECT E.NAME, D.DNAME
FROM EMP E NATURAL JOIN DEPT D;EMP와 DEPT에 이름이 같은 컬럼이 DEPT_ID 하나뿐이라면 위 USING 예시와 결과가 같다. 다만 만약 두 테이블에 우연히 이름이 같은 컬럼이 하나 더 있다면(예: 둘 다 CREATED_AT을 갖고 있다면), NATURAL JOIN은 그 컬럼까지 자동으로 조인 조건에 포함시켜버려 의도치 않은 결과가 나올 수 있다. 그래서 실무에서는 NATURAL JOIN보다 USING이나 ON을 명시하는 방식을 더 권장한다.
USING과 NATURAL JOIN은 ON과 함께 쓸 수 없다. NATURAL JOIN ... ON ...처럼 두 방식을 동시에 쓰면 조인 조건을 지정하는 방법이 중복 지정되어 구문 오류가 발생한다. 조인 조건 지정 방법(ON, USING, NATURAL)은 셋 중 하나만 골라 쓴다는 원칙을 기억해두자.
직접 해보기
00편에서 만든 환경에서 진행합니다. 아직 준비하지 않았다면 00편을 먼저 보세요.
직접 해보기 1: 조인 종류마다 행 수가 몇 개로 달라지는지 직접 세어보기
같은 EMP·DEPT 두 테이블에 조인 종류만 바꿔가며 COUNT(*)로 결과 행 수를 비교한다. 실행하기 전에 본문 설명을 떠올리며 각 줄이 몇 행을 낼지 먼저 예상해보자.
SELECT COUNT(*) FROM EMP E INNER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;
SELECT COUNT(*) FROM EMP E LEFT OUTER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;
SELECT COUNT(*) FROM EMP E RIGHT OUTER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;
SELECT COUNT(*) FROM EMP E FULL OUTER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID;
SELECT COUNT(*) FROM EMP E CROSS JOIN DEPT D;무엇을 보아야 하나: 다섯 숫자가 서로 다르게 나오는지, 그리고 INNER JOIN이 가장 작고 CROSS JOIN이 가장 크다는 크기 순서가 본문에서 설명한 계산과 맞아떨어지는지 확인한다.
왜 이걸 해보나: 기출은 “이 두 테이블을 이 조인으로 합치면 몇 행이 나오는가”를 직접 계산하게 만든다. 다섯 조인을 한 번에 돌려 숫자로 비교해두면 공식을 외우지 않아도 감으로 맞힐 수 있게 된다.
직접 해보기 2: NULL은 조인 조건에서 매칭되지 않는 것 확인하기
최지우(EMP_ID 4)는 DEPT_ID가 NULL이다. 이 사원이 INNER JOIN 결과에 나오는지 직접 확인해본다.
SELECT * FROM EMP WHERE EMP_ID = 4;-- 예상: 위 쿼리에서 최지우가 분명히 존재하는데, 이 쿼리에는 몇 행이 나올까?
SELECT E.NAME, D.DNAME
FROM EMP E
INNER JOIN DEPT D ON E.DEPT_ID = D.DEPT_ID
WHERE E.EMP_ID = 4;무엇을 보아야 하나: 첫 쿼리는 최지우 행이 분명히 존재함을 보여준다. 두 번째 쿼리는 DEPT_ID가 NULL이라 어떤 부서와도 비교 자체가 성립하지 않아 결과가 어떻게 되는지 본다.
왜 이걸 해보나: “NULL은 NULL끼리도 매칭되지 않는다”는 규칙을 글로만 읽으면 잘 와닿지 않는다. 분명히 존재하는 행이 조인 결과에서 통째로 사라지는 것을 직접 보면 이 규칙이 확실히 남는다.
자주 틀리는 점
- FULL OUTER JOIN 결과 건수를 단순히 “왼쪽 행 수 + 오른쪽 행 수”로 계산하는 실수가 잦다. 매칭된 행은 한 번만 세어야 하므로, “매칭 건수 + 양쪽 미매칭 건수”로 계산해야 한다.
- NULL이 조인 조건에 있으면 두 NULL끼리도 매칭되지 않는다는 점을 놓치기 쉽다. “값이 같으면 매칭”이 아니라 “값이 존재하고 서로 같아야 매칭”이다.
- CROSS JOIN과 조인 조건 없는 콤마 나열(FROM A, B)이 결과적으로 같다는 것을 모르고 지나치는 경우가 많다. 기출에서는 이 둘을 같은 개념으로 묶어 물어본다.
- NATURAL JOIN에 ON이나 USING을 덧붙이면 에러가 난다는 점은 실행 결과 예측이 아니라 “에러가 발생하는 문장 찾기” 유형으로 자주 출제된다.
- 이 단원은 정의를 묻기보다, 주어진 데이터로 결과 건수와 결과 값을 직접 계산해보라는 문항 비중이 압도적으로 높다. 공식을 외우기보다 이 문서의 표처럼 손으로 하나씩 짝을 맞춰보는 연습이 가장 효과적이다.
핵심 정리
- INNER JOIN은 양쪽에 짝이 있는 행만, LEFT/RIGHT/FULL OUTER JOIN은 각각 왼쪽·오른쪽·양쪽 테이블의 행을 조건 없이 보존한다.
- FULL OUTER JOIN 결과 건수 = INNER JOIN 매칭 건수 + 왼쪽 미매칭 건수 + 오른쪽 미매칭 건수.
- CROSS JOIN(카티전 곱) 결과 건수는 두 테이블 행 수의 곱이며, 조인 조건이 아예 없다.
- SELF JOIN은 같은 테이블을 별칭 두 개로 두 번 참조해 계층·순환 관계를 조회하는 방식이다.
- NULL은 조인 조건에서 어떤 값과도, 심지어 NULL끼리도 매칭되지 않는다.
- USING/NATURAL JOIN은 ON의 축약형이며 셋을 동시에 쓸 수 없다.
마무리 복습
참고 자료
- Oracle Database SQL Language Reference - Joins — INNER/OUTER/CROSS JOIN 표준 문법과 동작 원리