컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
db · 02

SQL JOIN

DBSQLJOIN

SQL JOIN은 두 개 이상의 테이블을 하나의 결과로 합치는 방법입니다. USERS와 ORDERS처럼 외래키로 연결된 테이블이 있을 때, 어느 쪽 데이터를 기준으로 얼마나 남길지에 따라 INNER / LEFT / RIGHT / FULL 네 가지 방식으로 나뉩니다. 벤 다이어그램으로 생각하면 이해하기 쉽습니다(다만 비유일 뿐입니다) — 두 원 중 어느 부분을 결과로 남기느냐의 차이일 뿐입니다.

핵심 개념

INNER JOIN

양쪽 테이블 모두에 값이 있는 행만 남깁니다. 벤 다이어그램에서 두 원이 겹치는 부분(교집합)입니다.

핵심 개념

LEFT JOIN

왼쪽(A) 테이블의 모든 행을 남기고, 짝이 없는 오른쪽(B) 값은 NULL로 채웁니다. 벤 다이어그램에서 왼쪽 원 전체입니다.

핵심 개념

RIGHT JOIN

오른쪽(B) 테이블의 모든 행을 남기고, 짝이 없는 왼쪽(A) 값은 NULL로 채웁니다. 벤 다이어그램에서 오른쪽 원 전체입니다.

핵심 개념

FULL OUTER JOIN

양쪽 테이블의 모든 행을 남기고, 짝이 없는 쪽은 NULL로 채웁니다. 벤 다이어그램에서 두 원의 합집합입니다.

핵심 개념

JOIN 조건

보통 외래키 = 기본키 형태로 조건을 겁니다. 예: ORDERS.user_id = USERS.id. 이 조건에 맞는 행끼리만 짝지어집니다.

핵심 개념

왜 NULL이 나올까

OUTER JOIN(LEFT/RIGHT/FULL)에서 짝이 없는 행도 결과에 포함시키다 보니, 상대 테이블 쪽 컬럼을 채울 값이 없어 NULL이 들어갑니다.

주문이 없는 사용자, 사용자가 없는 주문 — 이 시각화의 USERS에는 주문이 하나도 없는 사람이(박도윤) 있고, ORDERS에는 존재하지 않는 사용자를 가리키는 주문이(user_id=5) 하나 있습니다. 이 두 케이스가 각 JOIN에서 어떻게 다르게 처리되는지가 핵심입니다.
결과 행 개수부터 보세요 — 같은 데이터인데도 INNER는 2행, LEFT/RIGHT는 3행, FULL은 4행이 나옵니다. 어떤 행이 "더 나온" 것인지, 어떤 행이 "빠진" 것인지를 확인하면 차이가 분명해집니다.
MySQL에는 FULL OUTER JOIN이 없다 — MySQL은 FULL JOIN 문법을 직접 지원하지 않아서, 보통 LEFT JOIN 결과와 RIGHT JOIN의 unmatched 부분을 UNION으로 합쳐서 흉내 냅니다.
이 시각화에서 확인할 것 — 모드탭에서 JOIN 종류를 고르면 벤 다이어그램의 강조 영역이 바뀝니다. STEP을 눌러 결과 테이블이 한 행씩 채워지는 과정과, 그 행이 어떤 조건(일치 또는 NULL)으로 만들어졌는지 로그에서 확인하세요.

JOIN이 하는 일

개념적으로 JOIN은 두 테이블의 모든 행 조합(곱집합)을 만든 뒤 JOIN 조건에 맞는 조합만 남기는 연산입니다. 그 위에서 OUTER JOIN은 조건에 맞는 짝이 없는 행도 NULL로 채워 남깁니다. 실제로 DB가 곱집합을 다 만들지는 않고 해시나 인덱스 같은 방법으로 같은 결과를 더 빠르게 계산합니다. JOIN ... ON 형태의 명시적 구문은 SQL-92에서 도입되었다고 알려져 있습니다.

숫자로 확인하기

곱집합의 크기 — USERS가 100행, ORDERS가 1,000행이면 모든 조합은 100 × 1,000 = 100,000행입니다. 조건에 맞는 행만 남기므로 INNER JOIN 결과는 보통 이보다 훨씬 작습니다.
1:N에서 결과 행 수 — 사용자 한 명이 주문을 3건 가졌다면 INNER JOIN 결과에 그 사용자는 3행으로 나옵니다. 결과 행 수는 사용자 수가 아니라 짝이 맞는 주문 수를 따릅니다.
이 시각화의 행 수 — 같은 데이터에서 INNER는 2행, LEFT와 RIGHT는 3행, FULL은 4행입니다. 짝이 없는 사용자 1명과 짝이 없는 주문 1건이 각각 더해지기 때문입니다.

자주 하는 오해

JOIN은 벤 다이어그램의 집합 연산이다 — 벤 다이어그램은 어느 쪽 행이 남는지 이해하는 비유일 뿐입니다. JOIN은 행을 조건으로 짝짓는 연산이라, 한 행이 여러 행과 짝지어지면 결과 행이 늘어납니다.
LEFT JOIN에서 오른쪽 조건을 WHERE에 써도 같다 — WHERE는 JOIN 결과를 거른 뒤에 적용되어, 오른쪽 컬럼이 NULL인 행이 사라져 INNER JOIN처럼 동작합니다. 오른쪽 테이블의 조건을 유지하며 왼쪽 행을 남기려면 ON 절에 씁니다.
NULL끼리도 조인된다 — NULL = NULL은 참이 아니라 알 수 없음이어서, 조인 키가 NULL인 행은 서로 짝지어지지 않습니다.
RIGHT JOIN이 따로 꼭 필요하다 — 테이블 순서를 바꾸면 RIGHT JOIN은 LEFT JOIN으로 같은 행을 냅니다(SELECT *의 열 순서는 달라집니다). 그래서 LEFT JOIN만 쓰는 스타일도 많습니다.

면접에서 이렇게 물어요

면접 질문

INNER JOIN과 OUTER JOIN의 차이는 무엇인가요?

INNER JOIN은 양쪽에 짝이 있는 행만 남깁니다. OUTER JOIN은 한쪽(LEFT, RIGHT) 또는 양쪽(FULL)의 모든 행을 남기고, 짝이 없으면 상대 컬럼을 NULL로 채웁니다.

면접 질문

ON과 WHERE의 차이는 무엇인가요?

ON은 어떤 행끼리 짝지을지 정하는 조건이고, WHERE는 만들어진 결과를 거르는 조건입니다. INNER JOIN에서는 결과가 같지만 OUTER JOIN에서는 NULL로 채워진 행의 유지 여부가 달라집니다.

면접 질문

주문이 한 건도 없는 사용자를 어떻게 찾나요?

USERS LEFT JOIN ORDERS로 사용자를 모두 남긴 뒤, 오른쪽 기본키가 NULL인 행만 WHERE로 고릅니다. 짝이 없는 행만 남는 anti-join 패턴입니다.

면접 질문

DB는 JOIN을 어떻게 실행하나요?

중첩 루프, 해시, 병합(정렬 후 합치기) 방식이 대표적이고, 옵티마이저가 테이블 크기와 인덱스를 보고 비용이 낮은 방법을 고릅니다.

실제 시스템에서는

PostgreSQL의 실행 계획 — EXPLAIN 결과에 Nested Loop, Hash Join, Merge Join 같은 노드 이름으로 어떤 방식이 선택되었는지 나옵니다.
조인 키의 인덱스 — 조인에 쓰는 컬럼에 인덱스가 있으면 중첩 루프에서 상대 테이블을 빠르게 찾을 수 있습니다. 외래키 컬럼에는 PostgreSQL이 인덱스를 자동으로 만들지 않으므로 필요하면 직접 만듭니다.
FULL OUTER JOIN 지원 — PostgreSQL은 FULL JOIN을 지원하고, MySQL은 지원하지 않아서 LEFT JOIN과 RIGHT JOIN 결과를 UNION으로 합쳐 같은 결과를 만듭니다.

함께 보면 좋은 토픽

RDBMS 기초 — JOIN의 바탕이 되는 기본키와 외래키를 봅니다.
인덱스 — 조인 조건 컬럼에 인덱스가 왜 도움이 되는지 봅니다.
쿼리 실행 계획 — 옵티마이저가 조인 방법을 고르는 기준을 봅니다.
정규화 — 정규화로 나눈 테이블을 다시 합치는 이유를 봅니다.
뷰 / 구체화 뷰 — SELECT 문에 이름을 붙여 테이블처럼 조회하는 객체를 봅니다.

참고 자료

교재 — Silberschatz, Korth, Sudarshan, "Database System Concepts" 7판, "Intermediate SQL" 장

SQL — INNER / LEFT / RIGHT JOIN

-- INNER JOIN: 양쪽 모두 값이 있는 행만
SELECT u.id, u.name, o.id AS order_id, o.product
FROM users u
INNER JOIN orders o ON o.user_id = u.id;

-- LEFT JOIN: USERS 전부 + 짝 없는 ORDERS는 NULL
SELECT u.id, u.name, o.id AS order_id, o.product
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

-- RIGHT JOIN: ORDERS 전부 + 짝 없는 USERS는 NULL
SELECT u.id, u.name, o.id AS order_id, o.product
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;

세 쿼리의 FROM/ON 절은 완전히 동일하고, JOIN 키워드만 다릅니다 — 시각화의 벤 다이어그램에서 강조 영역이 바뀌는 것과 같은 지점입니다.
LEFT JOIN 결과에서 박도윤처럼 주문이 없는 사용자는 order_id, product가 NULL로 나옵니다.

Java — JPQL에서 LEFT JOIN FETCH로 N+1 방지

public interface UserRepository extends JpaRepository<User, Long> {

    // LEFT JOIN FETCH: 주문이 없는 사용자도 결과에 포함(= LEFT JOIN),
    // 연관된 Order를 한 번에 가져와 LazyInitializationException도 방지
    @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders")
    List<User> findAllWithOrders();
}

이 프로젝트 규칙상 연관 엔티티를 조회할 땐 항상 LEFT JOIN FETCH를 씁니다 — INNER JOIN FETCH를 쓰면 주문이 없는 User(박도윤)가 결과에서 통째로 빠져버리기 때문입니다.
즉 "연관 데이터를 함께 가져오고 싶다"는 이유만으로도, 의도치 않게 INNER JOIN의 필터링 효과가 발생할 수 있다는 점을 이 시각화의 INNER 탭과 비교해서 기억해두면 좋습니다.

SQL — MySQL에서 FULL OUTER JOIN 흉내내기

-- MySQL은 FULL OUTER JOIN 문법이 없어서 UNION으로 대체
SELECT u.id, u.name, o.id AS order_id, o.product
FROM users u
LEFT JOIN orders o ON o.user_id = u.id

UNION

SELECT u.id, u.name, o.id AS order_id, o.product
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;

LEFT JOIN 결과(USERS 전부)와 RIGHT JOIN 결과(ORDERS 전부)를 UNION으로 합치면, 겹치는 행은 자동으로 한 번만 남고 양쪽의 unmatched 행은 모두 살아남습니다.
이 시각화의 FULL 탭에서 4행(박도윤의 NULL 행 + user_id=5 주문의 NULL 행 포함)이 나오는 것과 정확히 같은 결과입니다.

Q1

INNER JOIN의 결과는?

'교집합'과 비슷한 개념입니다.
INNER JOIN은 조인 조건이 양쪽 테이블에서 모두 일치하는 행만 결과로 반환합니다.

Q2

LEFT JOIN에서 오른쪽 테이블에 일치하는 행이 없을 때 결과는?

왼쪽은 무조건 다 살아남습니다.
LEFT JOIN은 왼쪽 테이블의 모든 행을 유지하며, 오른쪽 테이블에 일치하는 행이 없으면 그 컬럼들을 NULL로 채웁니다.

Q3

FULL OUTER JOIN의 결과는?

왼쪽과 오른쪽 결과를 합친다고 생각하면 됩니다.
FULL OUTER JOIN은 LEFT JOIN과 RIGHT JOIN의 결과를 합친 것과 같아, 양쪽 테이블의 모든 행을 포함하며 상대편에 일치하는 값이 없으면 NULL로 채웁니다.