Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
“일단 LEFT JOIN” 습관에서 벗어나기
SQL의 JOIN은 쓰기는 쉽지만 잘못 고르면 데이터가 중복되거나 사라지는 까다로운 기능입니다. “헷갈리면 LEFT JOIN”으로 버티는 분들이 많지만, 각각의 차이를 제대로 이해해두면 예상치 못한 결과 행 수 때문에 헤매는 시간을 줄일 수 있습니다.
이 글은 JOIN 종류별 치트시트와 결과 행 수 예제를 중심으로 한 레퍼런스입니다. 마지막에는 여러 테이블 JOIN 작성법과, 기존 SQL을 JOIN 타입별로 다시 살펴보는 방법도 소개합니다.
JOIN 종류 치트시트
users(3건: Alice, Bob, Carol)와 orders(Alice 2건, Bob 1건, Carol 0건)를 예로 들어 반환되는 행을 비교해봅니다.
| JOIN 종류 | 반환되는 행 | Alice·Bob의 행 수 | Carol(주문 없음)의 처리 | 주문만 있는 행 |
|---|---|---|---|---|
INNER JOIN | 양쪽에 일치하는 행만 | Alice×2, Bob×1 | 제외됨 | 제외됨 |
LEFT JOIN | 왼쪽 테이블 전체 + 일치하는 오른쪽 | Alice×2, Bob×1 | Carol 행 1건(오른쪽은 NULL) | 제외됨 |
RIGHT JOIN | 오른쪽 테이블 전체 + 일치하는 왼쪽 | Alice×2, Bob×1 | 제외됨 | 존재하면 포함(왼쪽은 NULL) |
FULL JOIN | 양쪽 테이블 전체 | Alice×2, Bob×1 | Carol 행 1건 | 존재하면 포함 |
CROSS JOIN | 모든 조합(데카르트 곱) | 3명 × 3건 = 9행 | — | — |
INNER JOIN만이 유일하게 “걸러내는” JOIN입니다. 나머지는 전부 “한쪽 또는 양쪽의 모든 행을 보장”하는 방향으로 동작하며, 이 차이가 바로 행 수가 요동치는 원인입니다.
결과 행 수가 “늘어나는” 이유
가장 사고가 많이 나는 부분입니다. “JOIN했더니 행 수가 예상보다 많다”의 90%는 1대다 관계를 그대로 JOIN한 결과입니다.
SELECT users.name, orders.id
FROM users
INNER JOIN orders ON users.id = orders.user_id;
users 3건, orders 3건이어도 결과는 주문 테이블의 행 수만큼 나옵니다(Alice의 주문이 2건이면 Alice의 행도 2행). 집계할 때 이를 놓치고 COUNT(*)를 그대로 쓰면 사용자 수가 아니라 주문 수를 세게 됩니다.
-- 잘못됨: 사용자 수를 세려 했지만 "사용자×주문" 행 수가 나옴
SELECT COUNT(*) FROM users INNER JOIN orders ON users.id = orders.user_id;
-- 올바름: 중복을 제거한 사용자 수
SELECT COUNT(DISTINCT users.id) FROM users INNER JOIN orders ON users.id = orders.user_id;
“JOIN 전에 몇 건이었는지”를 항상 의식하고, JOIN 후 행 수가 그 정수배가 아니라면 조인 조건을 의심하는 것이 실무의 철칙입니다.
LEFT JOIN에서 가장 많이 걸리는 함정: WHERE절이 JOIN을 무력화한다
“주문이 없는 사용자도 포함하고 싶어서” LEFT JOIN을 썼는데, WHERE에 조건을 쓰는 순간 INNER JOIN과 같은 결과로 되돌아가는 것 — 이것이 두 번째로 흔한 사고입니다.
-- 함정: 겉보기엔 LEFT JOIN이지만 WHERE로 orders를 필터링하면 NULL 행이 사라짐
SELECT users.name, orders.status
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'completed'; -- Carol(NULL)이 여기서 걸러짐
Carol은 주문이 없으므로 orders.status는 NULL입니다. WHERE orders.status = 'completed'는 NULL = 'completed'를 평가하는데, SQL에서 NULL과의 비교는 항상 NULL(참도 거짓도 아님)로 취급되어 그 행은 결과에서 제외됩니다. 결과적으로 사실상 INNER JOIN과 같은 동작이 됩니다.
해결책은 조건을 ON절에 쓰는 것입니다.
-- 올바름: ON절에 조건을 넣으면 LEFT JOIN의 의미가 유지됨
SELECT users.name, orders.status
FROM users
LEFT JOIN orders ON users.id = orders.user_id AND orders.status = 'completed';
-- Carol의 행은 남고, orders 쪽은 전부 NULL이 됨
**“왼쪽 전체를 보장하고 싶다면 오른쪽 필터링은 ON절, 왼쪽 필터링은 WHERE절”**로 기억해두면 헷갈리지 않습니다.
여러 테이블 JOIN: 기준을 하나로 고정하기
3개 이상의 테이블을 JOIN할 때 헷갈린다면, “어느 테이블을 주어로 삼을지” 먼저 정하는 것이 요령입니다.
SELECT
users.name,
orders.id AS order_id,
order_items.quantity,
products.name AS product_name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
LEFT JOIN order_items ON orders.id = order_items.order_id
LEFT JOIN products ON order_items.product_id = products.id
WHERE users.deleted_at IS NULL;
FROM users를 기준으로 삼아 주문, 주문 상세, 상품 순으로 LEFT JOIN을 연쇄시켰습니다. 전부 users를 주어로 유지했기 때문에, “주문이 없는 사용자”도 “주문은 있지만 상세 데이터가 이상한 경우”도 모두 찾아낼 수 있습니다(도중에 INNER JOIN을 섞으면 그 지점에서 필터링이 발생하니 주의).
SELF JOIN: “별칭”으로 두 개의 테이블처럼 보이게 하기
같은 테이블끼리 조인하는 SELF JOIN은, 별칭(alias)으로 마치 서로 다른 두 테이블인 것처럼 다루는 것이 핵심입니다. 전형적인 예는 “직원과 그 상사” 같은 자기참조 구조입니다.
SELECT
emp.name AS employee_name,
mgr.name AS manager_name
FROM employees emp
LEFT JOIN employees mgr ON emp.manager_id = mgr.id;
employees에 emp와 mgr이라는 별칭을 부여함으로써 SQL에서는 “두 테이블의 JOIN”으로 쓸 수 있습니다. 상사가 없는(manager_id가 NULL인) 직원도 포함하고 싶으므로, 여기서도 LEFT JOIN이 자연스러운 선택입니다.
JOIN 조립하기·정리하기
여러 테이블 JOIN을 처음부터 작성하는 게 번거롭다면, **Visual SQL Builder**로 테이블과 JOIN 조건을 폼에서 조립할 수 있습니다. JOIN 종류(JOIN/LEFT JOIN/RIGHT JOIN/FULL JOIN)를 바꿔가며 생성 결과를 비교하면, 이 글의 치트시트와 함께 실제 동작을 확인하기 좋습니다.
기존의 복잡한 JOIN 쿼리를 읽어야 한다면, **SQL 포매터**로 정리하면 어떤 JOIN이 어느 테이블에 걸리는지 훨씬 잘 보입니다. 두 도구 모두 브라우저 안에서 완결되며, 테이블 이름이나 스키마 정보가 외부로 전송되지 않습니다.
자주 묻는 질문
INNER JOIN과 JOIN은 다른 건가요?
같습니다. 대부분의 RDBMS에서 JOIN은 INNER JOIN의 축약형으로 취급됩니다. 다만 코드 리뷰에서의 명확성을 위해 INNER JOIN이라고 명시적으로 쓰는 팀도 흔합니다.
RIGHT JOIN은 잘 안 쓴다고 들었는데 사실인가요?
A RIGHT JOIN B는 테이블 순서를 바꾼 B LEFT JOIN A와 같은 결과가 되므로, “항상 LEFT JOIN으로 통일하고 RIGHT JOIN은 쓰지 않는다”는 코딩 규칙을 가진 팀이 많습니다. 다만 기존 쿼리를 읽을 일은 있으니 의미는 알아두는 것이 좋습니다.
FULL JOIN을 지원하지 않는 데이터베이스가 있다고 들었어요
MySQL은 오랫동안 FULL JOIN을 직접 지원하지 않았습니다(UNION으로 LEFT JOIN과 RIGHT JOIN을 합성해서 대체합니다). PostgreSQL과 SQL Server는 표준으로 지원합니다. 사용 중인 데이터베이스의 지원 여부를 사전에 확인하세요.
JOIN 조건을 붙여넣어 확인하고 싶은데, 데이터가 전송되나요?
아니요. Visual SQL Builder와 SQL 포매터 모두 브라우저 안에서 완결되며, 입력한 테이블 이름이나 쿼리가 서버로 전송되지 않습니다.
정리
- INNER JOIN만이 “걸러내는” 방향. LEFT/RIGHT/FULL은 한쪽 또는 양쪽의 전체 행을 보장한다
- 1대다 JOIN은 행이 늘어난다. 집계 전에
COUNT(DISTINCT ...)를 검토하자 - LEFT JOIN에서 오른쪽을 필터링하려면 ON절, 왼쪽을 필터링하려면 WHERE절. WHERE절에 오른쪽 조건을 쓰면 LEFT JOIN이 사실상 INNER JOIN이 된다
- 여러 테이블은 기준 테이블을 고정하고 거기서부터 LEFT JOIN을 연쇄시키면 안정적이다
- SELF JOIN은 별칭으로 같은 테이블을 두 개처럼 보이게 한다