LabHub

SQL 실전 · 조인 · 이론

조인 — 어느 쪽을 남길 것인가

LabHub 에서 이어서 보기

한 줄 요약

조인은 두 집합의 행을 조건으로 짝짓는 연산이며, 실무 버그의 대부분은 짝이 없는 행을 어떻게 처리할지 잘못 정한 데서 나온다.

왜 이게 필요했나

정규화의 대가가 조인이다. 고객 정보와 주문 정보를 나눠 두었으니 함께 보려면 다시 붙여야 한다. 문제는 "짝이 없는 행"을 어떻게 할지다. 주문이 없는 고객을 결과에 남길 것인가 뺄 것인가. 이 결정이 곧 조인 종류다.

어떻게 동작하나

여기서 가장 중요한 함정을 짚어야 한다. LEFT JOIN 뒤의 WHERE 에 오른쪽 테이블 조건을 쓰면 그 순간 INNER JOIN 이 된다.

-- 모든 고객 + 그중 결제 완료 주문 (고객 400명 유지)SELECT c.id, count(o.id)FROM customers cLEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid'GROUP BY c.id;-- 결제 완료 주문이 있는 고객만 (고객 수가 줄어든다)SELECT c.id, count(o.id)FROM customers cLEFT JOIN orders o ON o.customer_id = c.idWHERE o.status = 'paid'GROUP BY c.id;

짝이 없는 행은 오른쪽 컬럼이 NULL 이 되는데, o.status = 'paid' 는 NULL 에 대해 미지를 돌려주므로 그 행들이 통째로 걸러진다. 왼쪽 외부 조인의 필터는 ON 절에, 왼쪽 테이블의 필터는 WHERE 절에 두는 것이 규칙이다.

집계와 결합될 때의 함정도 하나 더 있다. LEFT JOIN 뒤에 count(*) 를 쓰면 짝이 없는 행도 1로 세어 버린다. count(o.id) 처럼 조인된 쪽 컬럼을 세야 0 이 나온다. 집계 함수가 NULL 을 무시한다는 성질을 이용하는 것이다.

현장에서 만나는 모습

조인이 행을 늘린다는 사실도 자주 잊힌다. 주문 하나에 상세가 셋이면 조인 결과는 세 행이 되고, 그 상태에서 sum(o.total_amount) 를 하면 주문 금액이 세 배가 된다. 이런 팬아웃은 리포트 숫자를 조용히 부풀리는 대표적 원인이다. 해법은 집계를 먼저 하고 조인하거나, sum(DISTINCT ...) 가 아니라 서브쿼리로 미리 접는 것이다.

성능 면에서는 조인 알고리즘 세 가지를 알아 둘 만하다. 바깥쪽 행이 적고 안쪽에 인덱스가 있으면 중첩 루프, 등호 조인이고 한쪽을 메모리에 올릴 수 있으면 해시 조인, 양쪽이 이미 정렬되어 있으면 머지 조인 이 유리하다. 옵티마이저가 고르는 일이지만, 계획에서 중첩 루프의 반복 횟수가 수만 회를 넘는다면 바깥쪽 행 수 추정이 틀렸다는 신호다.

다음 실습에서 할 것

내부 조인, 외부 조인, 안티 조인, 자기 조인을 차례로 쓰고, ON 절과 WHERE 절에 같은 조건을 두었을 때 결과가 어떻게 갈리는지 직접 비교한다.