모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

14강 / 전체 24강

OUTER JOIN

10분 읽기 조회 0

LEFT/RIGHT OUTER JOIN으로 한쪽 테이블의 모든 행을 유지하고, 일치하지 않는 쪽은 NULL로 채워 누락 데이터를 찾습니다.

🎯 학습 목표

이 강을 마치면 LEFT JOIN과 RIGHT JOIN이 INNER JOIN과 어떻게 다른지 명확히 구분할 수 있습니다. "한 번도 주문하지 않은 회원 목록", "아직 팔리지 않은 도서 목록"처럼 조인에서 일치하지 않는 데이터를 찾는 실전 패턴을 작성할 수 있게 됩니다. 또한 OUTER JOIN 결과에 나타나는 NULL을 올바르게 처리하는 방법도 익힙니다.

💡 INNER JOIN의 한계

이 섹션에서는 INNER JOIN이 해결하지 못하는 상황과 OUTER JOIN이 필요한 이유를 살펴보겠습니다.

13강에서 members와 orders를 INNER JOIN했을 때, 주문이 한 번도 없는 회원은 결과에 나타나지 않았습니다. INNER JOIN은 양쪽 테이블 모두에 일치하는 행만 포함하기 때문입니다. 하지만 실무에서는 이런 질문이 자주 필요합니다. "아직 한 번도 주문하지 않은 회원은 누구인가?" "재고는 있지만 한 번도 팔리지 않은 도서는?" "주문 중 회원 정보가 없는 이상 데이터가 있는가?"

이런 요구사항에는 OUTER JOIN이 필요합니다. OUTER JOIN은 기준이 되는 테이블의 모든 행을 유지하고, 연결 조건이 일치하지 않는 쪽의 열은 NULL로 채워 결과에 포함합니다.

⬅️ LEFT JOIN — 왼쪽 테이블 전체 유지

이 섹션에서는 가장 많이 쓰이는 LEFT OUTER JOIN의 동작과 활용법을 살펴보겠습니다.

LEFT JOIN(LEFT OUTER JOIN)은 FROM 절에 지정한 왼쪽 테이블의 모든 행을 유지합니다. 오른쪽 테이블에 일치하는 행이 없으면 오른쪽 열들이 모두 NULL이 되어 결과에 포함됩니다. OUTER 키워드는 생략 가능합니다.

-- 모든 회원 + 주문 내역 (주문 없는 회원도 포함)
SELECT
  m.member_id,
  m.name          AS 회원이름,
  m.grade         AS 등급,
  o.order_id      AS 주문번호,
  o.order_date    AS 주문일,
  o.status        AS 상태
FROM members AS m
LEFT JOIN orders AS o ON m.member_id = o.member_id
ORDER BY m.member_id, o.order_date;

members가 왼쪽이므로 6명 회원이 모두 나타납니다. 주문이 있는 회원(1~5번)은 주문 수만큼 행이 반복됩니다. 만약 주문이 없는 회원이 있다면 그 회원의 order_id, order_date, status 열이 모두 NULL로 표시됩니다.

이제 "주문이 한 번도 없는 회원"만 골라냅니다. LEFT JOIN 후 오른쪽 테이블의 특정 열이 NULL인 행을 찾으면 됩니다.

-- 주문한 적 없는 회원 찾기
SELECT
  m.member_id,
  m.name   AS 회원이름,
  m.grade  AS 등급
FROM members AS m
LEFT JOIN orders AS o ON m.member_id = o.member_id
WHERE o.order_id IS NULL
ORDER BY m.member_id;

WHERE o.order_id IS NULL은 오른쪽 테이블과 일치하지 않은 행, 즉 주문이 없는 회원만 남깁니다. 이 패턴은 "A에는 있지만 B에는 없는" 데이터를 찾는 실무 핵심 패턴입니다.

마찬가지로 한 번도 주문되지 않은 도서를 찾아봅니다.

-- 한 번도 팔리지 않은 도서 찾기
SELECT
  b.book_id,
  b.title     AS 도서제목,
  b.category,
  b.price
FROM books AS b
LEFT JOIN orders AS o ON b.book_id = o.book_id
WHERE o.order_id IS NULL;

orders에 해당 book_id가 없는 도서, 즉 주문 내역이 전혀 없는 도서가 나타납니다.

🔢 LEFT JOIN + 집계

이 섹션에서는 LEFT JOIN과 GROUP BY를 결합해 모든 회원의 주문 통계를 집계하는 방법을 살펴보겠습니다.

INNER JOIN과 GROUP BY를 결합하면 주문이 있는 회원만 집계됩니다. 주문이 없는 회원도 "0건"으로 함께 보여주려면 LEFT JOIN을 사용해야 합니다.

-- 모든 회원의 주문 건수 (주문 없는 회원은 0건)
SELECT
  m.name              AS 회원이름,
  m.grade             AS 등급,
  COUNT(o.order_id)   AS 주문건수,
  COALESCE(SUM(o.quantity), 0) AS 총주문수량
FROM members AS m
LEFT JOIN orders AS o ON m.member_id = o.member_id
GROUP BY m.member_id, m.name, m.grade
ORDER BY 주문건수 DESC;

주문이 없는 회원은 o.order_id가 NULL이므로 COUNT(o.order_id)가 0이 됩니다(COUNT(*)를 쓰면 1이 되므로 반드시 COUNT(특정열)을 써야 합니다). SUM(o.quantity)는 NULL이 될 수 있으므로 COALESCE로 0으로 치환합니다.

모든 도서의 판매 통계(팔리지 않은 도서 포함)를 구합니다.

SELECT
  b.title                         AS 도서제목,
  b.category,
  COUNT(o.order_id)               AS 주문건수,
  COALESCE(SUM(o.quantity), 0)    AS 총판매수량
FROM books AS b
LEFT JOIN orders AS o ON b.book_id = o.book_id
GROUP BY b.book_id, b.title, b.category
ORDER BY 총판매수량 DESC;

모든 도서가 나타나며, 한 번도 팔리지 않은 도서는 주문건수 0, 총판매수량 0으로 표시됩니다.

➡️ RIGHT JOIN — 오른쪽 테이블 전체 유지

이 섹션에서는 RIGHT JOIN의 동작과 LEFT JOIN과의 관계를 살펴보겠습니다.

RIGHT JOIN(RIGHT OUTER JOIN)은 오른쪽 테이블의 모든 행을 유지하고, 왼쪽 테이블에 일치하는 행이 없으면 왼쪽 열들을 NULL로 채웁니다. LEFT JOIN과 좌우만 반대입니다.

-- orders를 왼쪽으로, members를 오른쪽으로 RIGHT JOIN
SELECT
  o.order_id,
  m.name     AS 회원이름,
  o.status
FROM orders AS o
RIGHT JOIN members AS m ON o.member_id = m.member_id
ORDER BY m.member_id;

결과는 앞서 작성한 FROM members LEFT JOIN orders와 동일합니다. RIGHT JOIN은 테이블 순서만 바꾼 LEFT JOIN입니다. 실무에서는 대부분 LEFT JOIN을 선호합니다. FROM에 쓰는 기준 테이블이 항상 왼쪽에 오는 것이 읽기 쉽기 때문입니다. RIGHT JOIN은 기존 쿼리에 JOIN을 추가할 때 순서를 바꾸기 어려운 상황에서 사용합니다.

중요한 점은 A LEFT JOIN B와 B RIGHT JOIN A는 항상 동일한 결과를 냅니다. 어느 쪽을 쓰든 결과는 같으므로 일관성을 위해 팀에서 하나를 정해 사용하는 것이 좋습니다.

🔄 FULL OUTER JOIN

이 섹션에서는 양쪽 테이블의 모든 행을 포함하는 FULL OUTER JOIN을 살펴보겠습니다.

FULL OUTER JOIN은 양쪽 테이블의 모든 행을 포함합니다. 일치하지 않는 쪽은 NULL로 채웁니다. LEFT JOIN과 RIGHT JOIN의 합집합입니다.

안타깝게도 SQLite는 FULL OUTER JOIN을 직접 지원하지 않습니다. 하지만 LEFT JOIN과 UNION을 결합해 동일한 결과를 만들 수 있습니다. UNION은 17강에서 배웁니다.

-- SQLite에서 FULL OUTER JOIN 효과 내기
SELECT m.name, o.order_id
FROM members AS m
LEFT JOIN orders AS o ON m.member_id = o.member_id

UNION

SELECT m.name, o.order_id
FROM orders AS o
LEFT JOIN members AS m ON o.member_id = m.member_id;

MySQL, PostgreSQL에서는 직접 FULL OUTER JOIN 키워드를 사용할 수 있습니다. 실무에서 FULL OUTER JOIN이 필요한 경우는 데이터 정합성 검증이나 마이그레이션 작업에서 주로 발생합니다.

⚠️ OUTER JOIN 주의사항

이 섹션에서는 OUTER JOIN 사용 시 자주 겪는 함정을 살펴보겠습니다.

첫 번째 주의사항은 WHERE 조건이 OUTER JOIN을 INNER JOIN으로 변환한다는 점입니다. LEFT JOIN 후 오른쪽 테이블의 NULL이 아닌 열을 WHERE 조건으로 쓰면, 일치하지 않는 행(NULL인 행)이 걸러져 INNER JOIN과 같은 결과가 됩니다.

-- OUTER JOIN의 효과가 사라지는 잘못된 패턴
SELECT m.name, o.order_id, o.status
FROM members AS m
LEFT JOIN orders AS o ON m.member_id = o.member_id
WHERE o.status = 'DONE';  -- NULL인 행이 WHERE에서 걸러짐 → INNER JOIN과 동일!

-- 의도가 "주문 없는 회원도 보여주되, 있다면 완료 주문만"이라면
-- ON 조건에 추가하거나 별도 처리 필요
SELECT m.name, o.order_id, o.status
FROM members AS m
LEFT JOIN orders AS o ON m.member_id = o.member_id
                       AND o.status = 'DONE';  -- ON에 추가하면 주문없는 회원도 유지

조인 조건(ON)과 필터 조건(WHERE)의 차이가 OUTER JOIN에서 중요하게 드러납니다. "일치 조건"은 ON에, "결과 필터"는 WHERE에 쓰는 원칙을 기억하세요.

두 번째는 NULL 처리에 COALESCE를 적극 활용하는 것입니다. LEFT JOIN 후 집계할 때 오른쪽 테이블의 열이 NULL이 되는 행에서 SUM이 NULL을 반환할 수 있습니다. COALESCE(SUM(열), 0)으로 NULL을 0으로 치환하는 습관을 들이세요.

📝 핵심 요약

이번 강에서 배운 내용을 정리하겠습니다.

종류동작언제 사용
INNER JOIN양쪽 일치하는 행만관련 데이터가 반드시 존재하는 경우
LEFT JOIN왼쪽 전체 + 오른쪽 일치 (없으면 NULL)왼쪽 테이블의 모든 행을 보존할 때
RIGHT JOIN오른쪽 전체 + 왼쪽 일치 (없으면 NULL)LEFT JOIN의 좌우 반전 (주로 LEFT JOIN 선호)
FULL OUTER JOIN양쪽 전체 (없으면 NULL)양쪽 모두를 보존해야 할 때 (SQLite는 UNION으로 대체)

핵심 패턴: "A에 있고 B에 없는" → A LEFT JOIN B WHERE B.pk IS NULL

다음 15강에서는 세 개 이상의 테이블을 동시에 조인하는 다중 조인의 실전 패턴을 심화 학습합니다. LEFT JOIN과 INNER JOIN을 혼합하거나 서로 다른 테이블에 여러 번 조인하는 복잡한 쿼리를 작성하는 방법을 배웁니다.

관련 주제

  • LEFT JOIN
  • RIGHT JOIN
  • OUTER JOIN
  • NULL 처리
  • 누락 데이터 조회
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • SQL·데이터베이스 입문 24강
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

SQL·데이터베이스 입문 24강

이 강의가 속한 시리즈는 총 24강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...