icon

안동민 개발노트

5장 : SQL 심화 — 조인과 서브쿼리

OUTER JOIN


INNER JOIN은 양쪽 테이블에서 조인 조건이 일치하는 행만 반환합니다. 하지만 실무에서는 주문이 없는 회원도 포함하여 보여달라, 담당자가 배정되지 않은 프로젝트도 목록에 나와야 한다라는 요구가 빈번합니다. 이처럼 일치하지 않는 행도 결과에 포함시키고 싶을 때, OUTER JOIN을 사용합니다.

OUTER JOIN의 핵심은 한쪽(또는 양쪽) 테이블의 모든 행을 보존하고, 매칭되지 않는 상대 테이블의 컬럼은 NULL로 채운다는 것입니다.

INNER JOIN vs OUTER JOIN 비교
INNER JOIN
┌─────┬─────┐
│  A  │  B  │
├─────┼─────┤
│  ○  │  ○  │  일치만 반환
└─────┴─────┘
매칭 안 되면 제외

OUTER JOIN
┌─────┬─────┐
│  A  │  B  │
├─────┼─────┤
│  ○  │  ○  │  일치
│  ○  │ NULL│  불일치도 포함
└─────┴─────┘
매칭 안 되면 NULL

OUTER JOIN의 세 가지 종류

OUTER JOIN은 어느 쪽 테이블을 보존하느냐에 따라 세 가지로 나뉩니다.

OUTER JOIN 종류
LEFT OUTER JOIN
┌─────┬─────┐
│  A  │  B  │
├─────┼─────┤
│  ○  │  ○  │  일치
│  ○  │ NULL│  A만
└─────┴─────┘
왼쪽 테이블 전체 보존

RIGHT OUTER JOIN
┌─────┬─────┐
│  A  │  B  │
├─────┼─────┤
│  ○  │  ○  │  일치
│ NULL│  ○  │  B만
└─────┴─────┘
오른쪽 테이블 전체 보존

FULL OUTER JOIN
┌─────┬─────┐
│  A  │  B  │
├─────┼─────┤
│  ○  │  ○  │  일치
│  ○  │ NULL│  A만
│ NULL│  ○  │  B만
└─────┴─────┘
양쪽 모두 보존
  • LEFT OUTER JOIN: 왼쪽 테이블의 모든 행을 보존합니다
  • RIGHT OUTER JOIN: 오른쪽 테이블의 모든 행을 보존합니다
  • FULL OUTER JOIN: 양쪽 테이블의 모든 행을 보존합니다

SQL에서 OUTER 키워드는 생략할 수 있습니다. LEFT JOIN이라고 쓰면 LEFT OUTER JOIN과 동일합니다.


LEFT JOIN 상세

LEFT JOIN은 가장 자주 사용되는 OUTER JOIN입니다. FROM 절에 오는 왼쪽 테이블의 모든 행을 유지하고, 오른쪽 테이블에서 매칭되는 행이 없으면 NULL로 채웁니다.

예제 테이블
-- 회원 테이블
-- user_id | username
-- 1       | 김철수
-- 2       | 이영희
-- 3       | 박민수

-- 주문 테이블
-- order_id | user_id | status
-- 1        | 1       | DELIVERED
-- 2        | 1       | SHIPPED
-- 3        | 2       | PAID
LEFT JOIN 기본
SELECT u.username, o.order_id, o.status
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;
실행 결과
username | order_id | status
---------|----------|----------
김철수   | 1        | DELIVERED
김철수   | 2        | SHIPPED
이영희   | 3        | PAID
박민수   | NULL     | NULL       ← 주문이 없지만 결과에 포함

박민수는 주문이 없지만 LEFT JOIN이므로 왼쪽 테이블(users)의 모든 행이 보존됩니다. 매칭되는 주문이 없기 때문에 order_id와 status가 NULL입니다.

주의할 점은 김철수처럼 매칭되는 행이 여러 개이면, 왼쪽 테이블의 같은 행이 여러 번 출력된다는 것입니다. 1:N 관계에서 LEFT JOIN을 하면 결과 행 수는 보존 테이블의 행 수 이상이 됩니다.


LEFT JOIN + IS NULL (Anti-Join 패턴)

매칭되지 않는 행만 찾기는 실무에서 매우 자주 쓰이는 패턴입니다.

주문이 없는 회원만 찾기
SELECT u.username
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;

이 패턴을 Anti-Join이라 부릅니다. A에는 있지만 B에는 없는 것을 찾을 때 사용합니다.

Anti-Join이 필요한 실무 상황
* 구매 이력이 없는 고객
* 댓글이 없는 게시글
* 담당자가 배정되지 않은 업무
* 최근 30일간 로그인하지 않은 사용자
* 카테고리에 속하지 않은 상품

WHERE 절에서 IS NULL을 체크할 때는 반드시 오른쪽 테이블의 기본키(PK) 또는 NOT NULL 컬럼을 사용해야 합니다. 원래 NULL이 가능한 컬럼으로 체크하면 원래 NULL인 행매칭되지 않아 NULL인 행을 구별할 수 없습니다.


Anti-Join 3가지 방법 비교

Anti-Join을 구현하는 방법은 세 가지가 있습니다.

방법 1: LEFT JOIN + IS NULL
SELECT u.username
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
방법 2: NOT EXISTS
SELECT u.username
FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o
    WHERE o.user_id = u.user_id
);
방법 3: NOT IN
SELECT u.username
FROM users u
WHERE u.user_id NOT IN (
    SELECT user_id FROM orders
    WHERE user_id IS NOT NULL
);
세 방법 비교
| 방법              | 장점              | 단점                         |
| ----------------- | ----------------- | ---------------------------- |
| LEFT JOIN+IS NULL | 직관적, 범용적    | 대용량에서 메모리 사용 많음  |
| NOT EXISTS        | NULL 안전, 효율적 | 서브쿼리 이해 필요           |
| NOT IN            | 가장 간결         | NULL이 있으면 결과 오류 발생 |

NOT IN에서 서브쿼리 결과에 NULL이 포함되면, SQL의 3값 논리에 의해 모든 비교가 UNKNOWN이 되어 결과가 아무 행도 반환되지 않습니다. 따라서 NOT IN을 사용할 때는 반드시 WHERE user_id IS NOT NULL 조건을 추가하거나, NOT EXISTS를 사용하는 것이 안전합니다.

NOT IN의 NULL 함정
-- orders에 user_id = NULL인 행이 있으면
-- 이 쿼리는 아무 결과도 반환하지 않음!
SELECT u.username
FROM users u
WHERE u.user_id NOT IN (
    SELECT user_id FROM orders  -- NULL 포함 가능
);

대부분의 옵티마이저는 LEFT JOIN + IS NULL과 NOT EXISTS를 동일한 실행 계획으로 변환합니다. 둘 중 팀에서 선호하는 스타일을 사용하면 됩니다.


RIGHT JOIN

RIGHT JOIN은 오른쪽 테이블의 모든 행을 보존합니다. LEFT JOIN에서 테이블 순서를 뒤집은 것과 동일합니다.

RIGHT JOIN
SELECT u.username, o.order_id, o.status
FROM orders o
RIGHT JOIN users u ON u.user_id = o.user_id;

위 쿼리는 아래 LEFT JOIN과 완전히 동일한 결과를 반환합니다.

동일한 LEFT JOIN
SELECT u.username, o.order_id, o.status
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;

실무에서는 RIGHT JOIN보다 LEFT JOIN을 훨씬 많이 사용합니다. 이유는 SQL을 왼쪽에서 오른쪽으로 읽기 때문에, 기준 테이블을 먼저 쓰고, 조인 대상을 뒤에 쓰는 LEFT JOIN이 더 자연스럽기 때문입니다. 3개 이상의 테이블을 조인할 때도 LEFT JOIN만으로 일관되게 작성하는 것이 가독성이 좋습니다.


FULL OUTER JOIN

FULL OUTER JOIN은 양쪽 테이블의 모든 행을 보존합니다. 왼쪽에만 있는 행, 오른쪽에만 있는 행, 양쪽 모두에 있는 행이 전부 결과에 포함됩니다.

FULL OUTER JOIN
SELECT e.name AS 직원명, d.dept_name AS 부서명
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;
실행 결과
직원명   | 부서명
---------|--------
김철수   | 개발팀      ← 양쪽 매칭
이영희   | 마케팅팀    ← 양쪽 매칭
박민수   | NULL        ← 부서가 없는 직원
NULL     | 경영지원팀  ← 직원이 없는 부서

FULL OUTER JOIN의 결과를 집합으로 표현하면 LEFT JOIN ∪ RIGHT JOIN입니다.


MySQL에서 FULL OUTER JOIN 구현

MySQL은 FULL OUTER JOIN을 직접 지원하지 않습니다. UNION을 사용하여 동일한 결과를 얻을 수 있습니다.

MySQL에서 FULL OUTER JOIN 대체
-- LEFT JOIN 결과
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id

UNION

-- RIGHT JOIN 결과 (LEFT에서 안 나온 행만 추가)
SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;

UNION은 중복을 제거하므로 양쪽에서 매칭된 행이 두 번 나오는 것을 방지합니다. 중복을 유지해야 하는 특수한 경우에는 UNION ALL과 WHERE 조건을 조합하여 구현합니다.

UNION ALL을 사용하는 정확한 FULL OUTER JOIN
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id

UNION ALL

SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id
WHERE e.emp_id IS NULL;  -- LEFT JOIN에서 이미 나온 행 제외

ON 절과 WHERE 절의 차이 (핵심!)

OUTER JOIN에서 가장 흔한 실수는 ON 절에 써야 할 조건을 WHERE 절에 쓰는 것입니다. 이 차이를 정확히 이해하는 것이 매우 중요합니다.

ON vs WHERE 처리 순서
1. FROM + JOIN + ON  →  조인 수행 (OUTER행 포함)
2. WHERE             →  조인 결과에서 필터링

ON 절의 조건은 조인 과정에서 적용되고, OUTER 행(NULL로 채워진 행)은 ON 조건과 무관하게 보존됩니다. 반면 WHERE 절의 조건은 조인이 끝난 후 적용되므로, OUTER 행도 필터링 대상이 됩니다.

ON 절에 조건을 두는 경우
-- 배송 완료된 주문만 조인하되, 모든 회원은 보존
SELECT u.username, o.order_id, o.status
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
                   AND o.status = 'DELIVERED';
결과 (ON 절)
username | order_id | status
---------|----------|----------
김철수   | 1        | DELIVERED   ← 배송 완료 주문만 조인됨
이영희   | NULL     | NULL        ← 주문 있지만 배송완료가 아니므로 매칭 안 됨
박민수   | NULL     | NULL        ← 주문 없음

이영희는 주문이 있지만 status가 DELIVERED가 아니므로 ON 조건에 매칭되지 않습니다. 그러나 LEFT JOIN이므로 이영희 행은 보존되고, 오른쪽 컬럼이 NULL로 채워집니다.

WHERE 절에 같은 조건을 두는 경우
SELECT u.username, o.order_id, o.status
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.status = 'DELIVERED';
결과 (WHERE 절)
username | order_id | status
---------|----------|----------
김철수   | 1        | DELIVERED   ← 이 행만 남음

WHERE 절에 o.status = 'DELIVERED'를 두면, 조인 후 NULL인 행(이영희, 박민수)은 o.status가 NULL이므로 WHERE 조건에 의해 제거됩니다. 결과적으로 LEFT JOIN이 INNER JOIN처럼 동작합니다.

핵심 규칙
* 보존 테이블의 필터:       WHERE 절에 작성
* 비보존 테이블의 필터:     ON 절에 작성 (OUTER 행 유지)
* 비보존 테이블의 필터:     WHERE 절에 작성 (OUTER 행 제거 → INNER JOIN과 동일)

이 규칙을 기억하면 OUTER JOIN에서 의도하지 않은 결과가 나오는 대부분의 문제를 해결할 수 있습니다.


여러 테이블의 OUTER JOIN

3개 이상의 테이블을 OUTER JOIN할 때는 조인 순서가 중요합니다.

3개 테이블 LEFT JOIN
-- 회원 → 주문 → 주문상세 순서로 조인
SELECT u.username, o.order_id, oi.product_name, oi.quantity
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id;
실행 흐름
1단계: users LEFT JOIN orders
  → 주문 없는 회원도 보존 (order_id = NULL)

2단계: (결과) LEFT JOIN order_items
  → 1단계에서 order_id가 NULL인 행은
    order_items와 매칭 불가 → product_name도 NULL

최종 결과
username | order_id | product_name | quantity
---------|----------|--------------|--------
김철수   | 1        | 노트북        | 1
김철수   | 2        | 마우스        | 2
이영희   | 3        | 키보드        | 1
박민수   | NULL     | NULL         | NULL

주의할 점은 두 번째 LEFT JOIN에서 첫 번째 조인 결과의 NULL 행이 올바르게 전파된다는 것입니다. 중간에 INNER JOIN을 섞으면 이전 LEFT JOIN으로 보존한 NULL 행이 제거될 수 있습니다.

INNER JOIN을 섞으면 위험
-- 첫 번째 LEFT JOIN으로 보존한 박민수 행이
-- 두 번째 INNER JOIN에서 제거됨!
SELECT u.username, o.order_id, oi.product_name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
INNER JOIN order_items oi ON o.order_id = oi.order_id;
-- 박민수: o.order_id = NULL → INNER JOIN 실패 → 행 제거

여러 테이블을 조인할 때는 LEFT JOIN을 일관되게 사용하는 것이 안전합니다.


OUTER JOIN과 집계 함수

OUTER JOIN과 집계 함수를 함께 사용할 때는 NULL 처리에 주의해야 합니다.

회원별 주문 수 (LEFT JOIN + COUNT)
-- 잘못된 방법
SELECT u.username, COUNT(*) AS order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.username;
잘못된 결과
username | order_count
---------|------------
김철수   | 2
이영희   | 1
박민수   | 1           ← 주문이 없는데 1? (NULL 행도 COUNT됨)

COUNT(*)는 NULL 행도 세므로, 주문이 없는 박민수도 1이 됩니다. 올바른 방법은 COUNT에 오른쪽 테이블의 컬럼을 지정하는 것입니다.

올바른 방법
SELECT u.username, COUNT(o.order_id) AS order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.username;
올바른 결과
username | order_count
---------|------------
김철수   | 2
이영희   | 1
박민수   | 0           ← COUNT(o.order_id)는 NULL을 세지 않음

SUM, AVG 등 다른 집계 함수는 NULL을 자동으로 무시하므로 이 문제가 발생하지 않습니다. 하지만 COUNT만은 COUNT(*)와 COUNT(컬럼)의 동작이 다르므로 주의해야 합니다.

COALESCE로 NULL 대체
SELECT u.username,
       COALESCE(SUM(o.total_amount), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.username;

SUM은 NULL을 무시하지만, 모든 값이 NULL이면 SUM 결과 자체가 NULL이 됩니다. COALESCE를 사용하면 이런 경우 0으로 대체할 수 있습니다.


SELF OUTER JOIN

자기 자신과 OUTER JOIN하는 것도 가능합니다. 계층 구조 데이터에서 상위 노드가 없는 최상위 항목을 포함할 때 자주 사용됩니다.

상사가 없는 직원도 포함 (조직도)
SELECT e.name AS 직원명,
       m.name AS 상사명
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
결과
직원명   | 상사명
---------|--------
김대표   | NULL      ← 상사 없음 (최상위)
이부장   | 김대표
박과장   | 이부장
최사원   | 박과장

상사가 없는 김대표도 LEFT JOIN 덕분에 결과에 포함됩니다. INNER JOIN이었다면 manager_id가 NULL인 김대표는 제외됩니다.


Oracle 전통 방식 (+)

Oracle에서는 ANSI 표준 JOIN 구문이 도입되기 전부터 (+) 표기법으로 OUTER JOIN을 지원했습니다.

Oracle (+) 문법
-- LEFT JOIN과 동일
SELECT u.username, o.order_id
FROM users u, orders o
WHERE u.user_id = o.user_id(+);

-- RIGHT JOIN과 동일
SELECT u.username, o.order_id
FROM users u, orders o
WHERE u.user_id(+) = o.user_id;

(+)는 NULL이 채워질 수 있는 쪽(비보존 테이블 쪽)에 붙입니다. LEFT JOIN이면 오른쪽에 (+), RIGHT JOIN이면 왼쪽에 (+)입니다.

(+) 표기법의 제한
* FULL OUTER JOIN을 표현할 수 없음
* OR 조건과 함께 사용할 수 없음
* 한 컬럼에 (+)를 여러 테이블에 적용할 수 없음
* 서브쿼리와 함께 사용할 수 없음

이러한 제한 때문에 Oracle 공식 문서에서도 ANSI 표준 JOIN 구문을 권장합니다. 하지만 레거시 코드에서 (+)를 자주 볼 수 있으므로 의미를 이해하고 있어야 합니다.


CROSS JOIN

CROSS JOIN은 두 테이블의 모든 행 조합(카테시안 곱)을 생성합니다. 조인 조건이 없습니다.

CROSS JOIN 기본
-- 3명 × 6상품 = 18행
SELECT u.username, p.name
FROM users u
CROSS JOIN products p;

CROSS JOIN은 의도적으로 사용하는 경우가 있습니다.

CROSS JOIN 활용: 달력 생성
-- 모든 월 × 모든 부서 조합 생성 (보고서 기본 틀)
SELECT m.month_name, d.dept_name,
       COALESCE(s.total_sales, 0) AS total_sales
FROM months m
CROSS JOIN departments d
LEFT JOIN monthly_sales s ON m.month_id = s.month_id
                          AND d.dept_id = s.dept_id;

이 패턴은 데이터가 없는 조합도 0으로 표시해야 하는 보고서를 만들 때 유용합니다. CROSS JOIN으로 모든 조합의 빈 틀을 만들고, LEFT JOIN으로 실제 데이터를 채우는 방식입니다.

CROSS JOIN 활용: 자릿수 생성
-- 0~9999까지의 숫자 테이블 생성
WITH digits AS (SELECT 0 AS d UNION ALL SELECT 1 UNION ALL SELECT 2
  UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
  UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
  UNION ALL SELECT 9)
SELECT a.d * 1000 + b.d * 100 + c.d * 10 + d.d AS num
FROM digits a CROSS JOIN digits b
CROSS JOIN digits c CROSS JOIN digits d;

의도하지 않은 CROSS JOIN은 성능 재앙을 일으킵니다. 10,000행 × 10,000행 = 1억 행이 생성됩니다. JOIN 조건을 빠뜨리지 않도록 항상 주의해야 합니다. 특히 암묵적 조인(FROM a, b) 구문에서 WHERE 절의 조인 조건을 누락하면 의도치 않은 CROSS JOIN이 됩니다.


NATURAL JOIN과 USING

NATURAL JOIN과 USING 절은 조인 조건을 간결하게 표현하는 방법입니다.

NATURAL JOIN
-- 같은 이름의 컬럼으로 자동 조인
SELECT username, order_id
FROM users
NATURAL JOIN orders;
-- user_id라는 같은 이름의 컬럼으로 자동 매칭
USING 절
SELECT username, order_id
FROM users
JOIN orders USING (user_id);
-- 조인 컬럼 이름이 같을 때 간결하게 작성
ON vs USING vs NATURAL JOIN
| 구문    | 특징                            | 권장 여부 |
| ------- | ------------------------------- | --------- |
| ON      | 가장 명시적, 다른 컬럼명도 가능 | ✅ 권장    |
| USING   | 같은 이름일 때 간결             | ○ 보통    |
| NATURAL | 모든 동명 컬럼으로 자동 조인    | ✗ 비권장  |

NATURAL JOIN은 위험합니다. 나중에 테이블에 같은 이름의 컬럼이 추가되면 예상치 못한 조인 조건이 생겨 결과가 바뀝니다. 명시적인 ON 절을 사용하는 것이 안전합니다.

USING 절의 특이한 점은 조인 컬럼이 결과에 한 번만 나타난다는 것입니다. ON 절로 조인하면 양쪽 테이블의 user_id가 모두 나타나지만, USING으로 조인하면 user_id가 한 번만 나타납니다.


LATERAL JOIN (CROSS APPLY)

일부 DBMS에서는 LATERAL JOIN이라는 고급 조인을 지원합니다. 왼쪽 테이블의 각 행에 대해 서브쿼리를 실행할 수 있습니다.

LATERAL JOIN (PostgreSQL)
-- 각 부서의 최근 주문 3건만 조인
SELECT d.dept_name, recent.order_id, recent.total
FROM departments d
LEFT JOIN LATERAL (
    SELECT o.order_id, o.total
    FROM orders o
    WHERE o.dept_id = d.dept_id
    ORDER BY o.order_date DESC
    LIMIT 3
) recent ON TRUE;
CROSS APPLY (SQL Server)
-- SQL Server에서 동일한 기능
SELECT d.dept_name, recent.order_id, recent.total
FROM departments d
OUTER APPLY (
    SELECT TOP 3 o.order_id, o.total
    FROM orders o
    WHERE o.dept_id = d.dept_id
    ORDER BY o.order_date DESC
) recent;

LATERAL JOIN은 각 행마다 조건이 다른 서브쿼리를 조인할 때 유용합니다. 일반 조인으로는 표현하기 어려운 부서별 상위 N개같은 패턴을 깔끔하게 작성할 수 있습니다.


OUTER JOIN 성능 고려

OUTER JOIN은 INNER JOIN보다 일반적으로 느립니다. 매칭되지 않는 행도 보존해야 하므로 옵티마이저의 선택지가 제한되기 때문입니다.

성능 최적화 팁
1. 필요할 때만 OUTER JOIN 사용
   → INNER JOIN으로 충분하면 INNER JOIN을 쓸 것

2. 인덱스 확인
   → 조인 컬럼에 인덱스가 있어야 함
   → 특히 비보존 테이블의 조인 컬럼

3. 불필요한 컬럼 제거
   → SELECT *를 피하고 필요한 컬럼만 명시

4. 조건을 ON에 넣을지 WHERE에 넣을지 신중히 결정
   → 잘못된 위치의 조건은 성능과 결과 모두에 영향

5. FULL OUTER JOIN 대신 UNION 고려
   → 일부 DBMS에서 UNION이 더 효율적

흔한 실수 정리

OUTER JOIN에서 자주 하는 실수
1. WHERE에서 비보존 테이블 컬럼 필터링
   → LEFT JOIN이 INNER JOIN으로 변질
   → 해결: ON 절로 조건 이동

2. COUNT(*)로 OUTER JOIN 결과 집계
   → NULL 행도 1로 카운트됨
   → 해결: COUNT(오른쪽_테이블.컬럼) 사용

3. NOT IN에 NULL 포함
   → 전체 결과가 공집합
   → 해결: NOT EXISTS 또는 IS NOT NULL 추가

4. 여러 테이블 조인에서 INNER JOIN 혼용
   → 앞선 LEFT JOIN의 보존 행이 제거됨
   → 해결: LEFT JOIN 일관 사용

5. NATURAL JOIN 사용
   → 컬럼 추가 시 의도치 않은 조인 조건 변경
   → 해결: 명시적 ON 절 사용

OUTER JOIN 전체 정리

OUTER JOIN 핵심 요약
┌────────────────────────────────────────────────────────────┐
│ LEFT JOIN  : 왼쪽 전체 보존, 오른쪽 NULL 채움              │
│ RIGHT JOIN : 오른쪽 전체 보존, 왼쪽 NULL 채움              │
│ FULL JOIN  : 양쪽 전체 보존, 없는 쪽 NULL                  │
│ CROSS JOIN : 조건 없이 모든 조합 (카테시안 곱)             │
├────────────────────────────────────────────────────────────┤
│ Anti-Join  : LEFT JOIN + WHERE pk IS NULL                  │
│ ON 조건    : 조인 시 적용, OUTER 행 보존                   │
│ WHERE 조건 : 조인 후 적용, OUTER 행도 필터링               │
│ COUNT      : COUNT(*)는 NULL 포함, COUNT(col)은 제외       │
├────────────────────────────────────────────────────────────┤
│ 권장: LEFT JOIN + ON 절 + 명시적 컬럼 지정                 │
│ 비권장: RIGHT JOIN(순서 바꿔서 LEFT), NATURAL JOIN         │
└────────────────────────────────────────────────────────────┘

다음 절에서는 쿼리 안에 쿼리를 넣는 서브쿼리를 다루겠습니다.

목차