데이터베이스 용어 사전
SQLJOIN · LEFT JOIN · INNER JOIN · OUTER JOIN

조인

두 테이블을 조건으로 엮어 한 결과로 만드는 연산. INNER는 양쪽에 있는 것만, OUTER는 한쪽만 있어도 남긴다.

정규화로 쪼갠 테이블을 다시 엮어 하나의 결과로 만드는 연산.

종류와 남는 행

INNER JOIN   양쪽에 짝이 있는 행만
LEFT  JOIN   왼쪽은 다 남기고, 짝이 없으면 오른쪽을 NULL 로 채운다
RIGHT JOIN   반대
FULL  JOIN   양쪽 다 남긴다 (MySQL 은 지원하지 않아 UNION 으로 흉내 낸다)
CROSS JOIN   조건 없이 모든 조합 (곱집합)
-- 주문이 없는 회원까지 포함해 주문 수를 센다
SELECT m.id, m.name, count(o.id) AS order_count
  FROM member m
  LEFT JOIN orders o ON o.member_id = m.id
 GROUP BY m.id, m.name;
-- count(o.id) 를 쓴다. count(*) 로 하면 주문 없는 회원도 1이 된다

마지막 주석이 실무에서 가장 자주 틀리는 지점이다 — LEFT JOIN 뒤의 count(*)는 NULL 행도 1로 센다.

LEFT JOIN을 INNER로 만들어 버리는 함정

-- ❌ WHERE 에 오른쪽 테이블 조건을 걸면 NULL 행이 걸러져 INNER 가 된다
SELECT * FROM member m
  LEFT JOIN orders o ON o.member_id = m.id
 WHERE o.status = 'PAID';        -- 주문 없는 회원의 o.status 는 NULL → 탈락

-- ✅ 조건을 ON 절로 옮긴다
SELECT * FROM member m
  LEFT JOIN orders o ON o.member_id = m.id AND o.status = 'PAID';

ON은 조인할 때 쓰는 조건, WHERE는 조인 결과를 거르는 조건이다. INNER JOIN에서는 둘이 같지만 OUTER JOIN에서는 전혀 다르다.

행 수가 불어나는 경우

-- 1:N 조인은 왼쪽 행이 오른쪽 개수만큼 복제된다
SELECT m.id, sum(o.amount)
  FROM member m JOIN orders o ON o.member_id = m.id
  JOIN coupon c ON c.member_id = m.id     -- 쿠폰이 3장이면 주문 금액이 3배로 합산된다
 GROUP BY m.id;

두 개의 1:N을 동시에 조인하면 곱집합이 되어 집계가 부풀려진다. 각각 서브쿼리로 집계한 뒤 조인하는 것이 정석이다.

SELECT m.id, o.total, c.cnt
  FROM member m
  LEFT JOIN (SELECT member_id, sum(amount) total FROM orders GROUP BY 1) o ON o.member_id = m.id
  LEFT JOIN (SELECT member_id, count(*) cnt   FROM coupon GROUP BY 1) c ON c.member_id = m.id;

세미 조인 — 존재만 확인할 때

-- 주문한 적 있는 회원 — JOIN 을 쓰면 주문 수만큼 중복된다
SELECT DISTINCT m.* FROM member m JOIN orders o ON o.member_id = m.id;   -- DISTINCT 비용

-- EXISTS 는 첫 일치에서 멈춘다. 중복도 안 생긴다
SELECT * FROM member m WHERE EXISTS (SELECT 1 FROM orders o WHERE o.member_id = m.id);

면접 함정

  • "조인은 느리니 나눠 조회한다" → N+1이 되어 훨씬 느려진다. 왕복이 비싸다.
  • "LEFT JOIN이 INNER JOIN보다 항상 느리다" → 계획에 따라 다르다. 잘못된 WHERE로 사실상 INNER가 되고 있는 경우가 더 흔한 문제다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — SQL 쿼리문 — SELECT·조인·서브쿼리·집계·윈도우·CTE