SQL JOIN 완전 정복 — INNER, LEFT, RIGHT, FULL 한번에

한줄 요약: INNER / LEFT / RIGHT / FULL JOIN의 차이를 실무 예제로 한 번에 이해하고, 어떤 상황에 어떤 JOIN을 써야 하는지 바로 판단할 수 있게 됩니다.

JOIN 쿼리 짜다가 결과가 이상하게 나와서 한 시간 넘게 디버깅한 적 있으신가요? 저도 처음엔 그냥 INNER JOIN만 쓰다가 데이터가 사라지는 이유를 몰라서 한참 헤맸습니다.

JOIN이 뭔지 모르면, 데이터가 왜 사라지는지도 모릅니다

JOIN은 두 개 이상의 테이블을 특정 조건으로 연결해서 하나의 결과로 만드는 SQL의 핵심 기능입니다.
쉽게 말하면, 엑셀 시트 두 개를 공통 열 기준으로 붙이는 것과 같습니다.
문제는 JOIN 종류마다 ‘어떤 행을 포함하고 제외할지’가 완전히 다르다는 점입니다.

INNER JOIN은 양쪽 테이블에 모두 매칭되는 행만 남깁니다.
한쪽에만 있는 데이터는 결과에서 통째로 사라집니다.
이걸 모르면 ‘왜 이 고객 데이터가 없지?’ 하는 상황이 생깁니다.

LEFT JOIN은 왼쪽 테이블의 모든 행을 기준으로 오른쪽을 붙입니다.
오른쪽에 매칭이 없으면 NULL로 채웁니다.
RIGHT JOIN은 반대로 오른쪽 기준입니다. 실무에서는 LEFT JOIN이 훨씬 많이 쓰입니다.

FULL OUTER JOIN은 양쪽 모두의 행을 포함합니다.
매칭 안 되는 쪽은 NULL로 채워집니다.
두 테이블 간 데이터 불일치를 찾을 때 특히 유용합니다.

실전 예제 — 주문 없는 고객도 찾아내야 할 때

고객 테이블과 주문 테이블을 JOIN하는 가장 흔한 실무 시나리오로 네 가지 JOIN을 모두 비교해 보겠습니다.

-- 테이블 구조 예시
-- customers: id, name
-- orders: id, customer_id, amount

-- 1. INNER JOIN: 주문한 고객만 조회 (양쪽 매칭된 행만)
SELECT
  c.id,
  c.name,
  o.amount
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
-- 결과: 주문 이력이 없는 고객은 제외됩니다

-- 2. LEFT JOIN: 모든 고객 조회 (주문 없으면 NULL)
SELECT
  c.id,
  c.name,
  o.amount  -- 주문 없으면 NULL
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
-- 결과: 주문 안 한 고객도 포함, amount는 NULL

-- 3. 주문 한 번도 안 한 고객만 뽑기 (LEFT JOIN 활용)
SELECT
  c.id,
  c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.customer_id IS NULL;  -- 매칭 안 된 행 필터링

-- 4. RIGHT JOIN: 모든 주문 기준 조회
SELECT
  c.name,
  o.id AS order_id,
  o.amount
FROM customers c
RIGHT JOIN orders o ON c.id = o.customer_id;
-- 결과: 고객 정보 없는 주문도 포함 (데이터 정합성 확인용)

-- 5. FULL OUTER JOIN: 양쪽 모두 포함 (MySQL은 UNION으로 대체)
SELECT c.id, c.name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
UNION
SELECT c.id, c.name, o.amount
FROM customers c
RIGHT JOIN orders o ON c.id = o.customer_id;
-- MySQL은 FULL OUTER JOIN 미지원 → UNION으로 구현

코드를 보면 JOIN 뒤에 ON 절로 연결 조건을 지정합니다. 여기서 c.id = o.customer_id가 두 테이블을 이어주는 키입니다.
LEFT JOIN + WHERE 매칭컬럼 IS NULL 패턴은 실무에서 정말 자주 씁니다. ‘이쪽에만 있고 저쪽엔 없는 데이터’를 찾는 용도로 활용하면 됩니다.
⚠️ MySQL은 FULL OUTER JOIN을 직접 지원하지 않으므로 UNION으로 대체해야 합니다. PostgreSQL이나 MSSQL은 그냥 써도 됩니다.

JOIN 쓸 때 가장 많이 하는 실수 3가지

사실 저도 이 부분에서 실수한 적이 있어요. 특히 첫 번째 실수는 꽤 오래 모르고 지나쳤습니다.

  • 잘못된 방법: WHERE 절로 JOIN 조건을 거는 것. FROM a, b WHERE a.id = b.a_id 같은 구식 문법은 가독성이 떨어지고 OUTER JOIN과 혼용 시 의도치 않은 결과가 나옵니다. 특히 팀 프로젝트에서 유지보수가 어렵습니다.
  • 올바른 방법: 반드시 JOIN ... ON 문법을 사용하세요. LEFT JOIN orders o ON c.id = o.customer_id처럼 명시적으로 작성하면 의도가 명확하고 실수를 줄일 수 있습니다.
  • 꿀팁: LEFT JOIN을 쓸 때 WHERE 절에 오른쪽 테이블 컬럼으로 필터를 걸면 INNER JOIN처럼 동작합니다. 예를 들어 LEFT JOIN orders o ON ... WHERE o.amount > 0은 NULL 행이 제거되어 사실상 INNER JOIN이 됩니다. 의도한 게 맞는지 꼭 확인하세요.

⚠️ JOIN 성능도 신경 써야 합니다

JOIN은 강력하지만 무분별하게 쓰면 쿼리가 느려집니다.
JOIN 대상 컬럼에 인덱스(INDEX)가 없으면 테이블 전체를 스캔합니다.
수십만 건 이상 데이터에서 인덱스 없이 JOIN하면 몇 초에서 몇 분까지 걸릴 수 있습니다.

ON c.id = o.customer_id에서 customer_id에 인덱스가 있는지 확인하세요.
없다면 CREATE INDEX idx_orders_customer_id ON orders(customer_id);로 추가하면 됩니다.
⚠️ 인덱스 추가는 조회 속도를 올리지만 INSERT/UPDATE 속도에 영향을 줄 수 있으니 상황에 맞게 판단하세요.

✏️ 퀴즈 — 어떤 JOIN을 써야 할까요?

아래 상황에서 어떤 JOIN을 사용해야 할지 생각해 보세요.

상황: 상품 테이블(products)과 리뷰 테이블(reviews)이 있습니다.
리뷰가 하나도 없는 상품도 포함해서 전체 상품 목록과 리뷰 수를 조회하고 싶습니다.
어떤 JOIN을 써야 할까요? 정답은 댓글에 남겨보세요!

힌트: 리뷰가 없는 상품도 결과에 포함되어야 한다면, 기준 테이블이 어느 쪽인지 생각해 보세요.

정리하며

JOIN은 SQL에서 가장 자주 쓰이는 기능인 만큼 확실히 이해해 두면 실무에서 정말 든든합니다.
INNER JOIN은 교집합, LEFT/RIGHT JOIN은 한쪽 기준 전체 포함, FULL OUTER JOIN은 합집합이라고 기억하면 헷갈리지 않습니다.
어떤 데이터를 ‘포함’할지, ‘제외’할지를 먼저 결정하고 JOIN 종류를 고르는 습관을 들이세요.

  • 이 글의 핵심: JOIN 종류마다 포함되는 행이 다르므로 결과를 먼저 그려보고 쿼리를 작성하세요.
  • 다음 단계: GROUP BY + JOIN 조합, 서브쿼리 vs JOIN 성능 비교, 다중 테이블 JOIN 최적화를 공부해 보세요.

참 쉽죠?

댓글 남기기