JOIN 4종 — INNER·LEFT·RIGHT·FULL
관계형 DB 의 진가. 4종 JOIN 그림 비교와 카티시안 곱의 함정.
관계형 DB 의 "관계" 가 살아나는 순간이 JOIN 입니다. 두 테이블을 공통 키로 연결해 한 결과로 — 이게 안 되면 매번 N+1 쿼리 지옥. 9편은 4가지 JOIN 의 차이를 그림과 예제로 정확히 보고, 가장 흔한 사고 카티시안 곱 도 정리합니다.
실습 테이블 — 사용자와 주문:
users orders
───── ──────
id name id user_id total
1 준성 1 1 10000
2 홍길동 2 1 5000
3 김자바 3 2 30000
4 99 99000 ← 고아 데이터(없는 user)
INNER JOIN — 양쪽에 다 있는 것만
SELECT u.name, o.id AS order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id;
-- 결과 (3 행) — user_id=99 와 김자바(주문 없음)는 빠짐
-- 준성 | 1 | 10000
-- 준성 | 2 | 5000
-- 홍길동| 3 | 30000
JOIN 만 쓰면 INNER JOIN 입니다. 양쪽 테이블 모두 매치되는 행만 결과에 포함.
LEFT JOIN — 왼쪽은 다, 오른쪽은 있으면
SELECT u.name, o.id AS order_id, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
-- 결과 (4 행) — 주문 없는 김자바도 NULL 로 포함
-- 준성 | 1 | 10000
-- 준성 | 2 | 5000
-- 홍길동| 3 | 30000
-- 김자바| NULL | NULL ← 주문 없음
-- "주문이 없는 사용자" 찾기 (anti-join)
SELECT u.* FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
LEFT JOIN + IS NULL = anti-join. "왼쪽에는 있지만 오른쪽에는 없는" 행을 찾는 정통 패턴. NOT EXISTS 도 같은 의미.
RIGHT JOIN — 거의 안 쓴다
SELECT u.name, o.id AS order_id
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;
-- 결과 — orders 의 모든 행 + users 매치 (없으면 NULL)
-- 준성 | 1
-- 준성 | 2
-- 홍길동 | 3
-- NULL | 4 ← user_id=99 는 users 에 없음
RIGHT 는 LEFT 와 거울. 가독성 차원에서 거의 항상 LEFT 로 적는 게 컨벤션 — FROM/JOIN 순서만 바꾸면 됩니다.
FULL OUTER JOIN — 양쪽 다 (없으면 NULL)
SELECT u.name, o.id AS order_id
FROM users u
FULL OUTER JOIN orders o ON o.user_id = u.id;
-- 결과 — INNER + LEFT 만 + RIGHT 만 합집합
-- 준성 | 1
-- 준성 | 2
-- 홍길동 | 3
-- 김자바 | NULL ← 주문 없는 사용자
-- NULL | 4 ← 사용자 없는 주문
데이터 동기화·차이 검사에서 쓸 때 있지만 일상 쿼리에서는 드물게 사용.
4종 한 그림 (텍스트로)
| JOIN | 왼쪽만 | 양쪽 매치 | 오른쪽만 |
|---|---|---|---|
| INNER | ✗ | ✓ | ✗ |
| LEFT | ✓ (NULL) | ✓ | ✗ |
| RIGHT | ✗ | ✓ | ✓ (NULL) |
| FULL OUTER | ✓ (NULL) | ✓ | ✓ (NULL) |
ON vs USING — 조건 표기
-- ON — 일반적 (다른 컬럼 이름끼리도 가능)
JOIN orders o ON o.user_id = u.id
-- USING — 컬럼 이름이 같을 때만 (Postgres 가 컬럼 합쳐줌)
SELECT u.name, o.id
FROM users u
JOIN orders o USING (user_id); -- u.user_id 와 o.user_id 가 같은 이름
self-join — 같은 테이블을 자기 자신과
-- 직원-매니저 (manager_id 가 같은 employees.id 참조)
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;
계층 구조, 시간차 비교(어제 vs 오늘), 짝짓기 등에서 등장합니다. 별칭이 필수.
카티시안 곱 — 잘못된 JOIN 의 함정
-- ❌ ON 조건 빼먹기
SELECT u.name, o.id FROM users u, orders o;
-- 결과: users.count × orders.count = 3 × 4 = 12 행
-- 모든 조합이 다 나옴 ← 사고
-- ❌ 잘못된 조건 (조건이 거의 항상 참)
JOIN orders o ON 1 = 1
-- ✅ 항상 매칭 키 명시
JOIN orders o ON o.user_id = u.id
대량 사고 1번지. 결과 행 수가 예상보다 훨씬 많으면 99% JOIN 조건이 빠지거나 잘못된 것입니다. WHERE 추가 전에 SELECT COUNT(*) 로 행 수부터 확인하는 습관.
여러 테이블 — 3개 이상 JOIN
SELECT u.name, o.id, oi.product_id, p.name AS product
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE u.id = 1;
실무 쿼리는 보통 3-5 개 테이블을 엮습니다. 너무 많아지면(7+ 테이블) 한 번 더 생각 — 보통은 분해해서 CTE 또는 뷰가 더 깔끔합니다(10·13편).
10편 — 서브쿼리와 CTE(WITH)
중첩 SELECT 와 가독성 좋은 WITH 절. 재귀 CTE 한 줄까지.
이전: 8편 GROUP BY · 현재: 9편 (SQL 기초) · 다음 → 10편 서브쿼리·CTE · 진행: 9/24