쪼개진 테이블을 다시 하나로,
JOIN — 두 개만 제대로
여기가 진짜 SQL의 시작입니다. 실무 데이터는 회원 정보는 회원 테이블에, 주문 내역은 주문 테이블에 — 여러 테이블에 쪼개져 있고, 이걸 옆으로 합치는 명령이 JOIN입니다. 종류는 4~5개지만 현실에서 쓰는 건 거의 INNER JOIN과 LEFT JOIN 둘뿐 — 이 강의에선 두 개만 제대로 익힙니다.
이번 섹션의 여정
완료 ✓
완료 ✓
지금 여기
AI가 가장 자주 틀리는 패턴
섹션 3 마무리
강의 메인
데이터는 쪼개져 있습니다 — 그래서 JOIN
지금까지 다섯 강의 내내 한 테이블만 다뤘죠. 근데 실무 데이터는 그렇게 생기지 않았습니다. 회원 정보는 members에, 주문 내역은 orders에 — 여러 테이블에 쪼개져 있어요. “회원 이름과 그 회원의 주문일을 한 표로 보고 싶다” 는 순간, 두 테이블을 합쳐야 합니다. 그게 JOIN — “두 테이블을 어떤 컬럼 기준으로 옆으로 붙인다” 는 명령입니다.
핵심은 “어떤 컬럼 기준으로?” 라는 질문이에요. 답은 우리가 2-3에서 본 그 구조 — 부모-자식 관계입니다.
members의 member_id가 기본키(PK), orders의 member_id가 그걸 가리키는 외래키(FK)죠. 그래서 JOIN의 ON 조건은 거의 항상 “부모ID = 자식ID” 입니다.
... ON m.member_id = o.member_idON 부모ID = 자식ID. 이 한 줄이 JOIN의 90%입니다. 어떤 JOIN을 만나도 — 먼저 부모와 자식이 누군지부터 찾으세요.
이 기준이 왜 중요하냐면 —
800행의 무의미한 조합
ON 없이 붙이면 회원 20명 × 주문 40건 — 모든 조합 800행이 쏟아집니다. 김철수 옆에 남의 주문이 붙어 있는, 아무 의미 없는 표.
정확히 40행 — 주문과 주인이 짝지어짐
각 주문이 자기 주인 회원하고만 연결됩니다. ON 조건이 바로 이 “붙이는 기준”을 정하는 자리예요.
INNER JOIN — 양쪽 다 매칭되는 행만
첫 번째 무기부터 실행해 볼게요. 회원 이름 + 그 회원의 주문ID + 주문일을 한 줄로 묶습니다.
SELECT
m.member_name,
o.order_id,
o.ordered_at
FROM
members m
INNER JOIN orders o
ON m.member_id = o.member_id;| member_name | order_id | ordered_at |
|---|---|---|
| 김철수 | 1 | 2025-02-01 |
| 김철수 | 6 | 2025-04-10 |
| 이영희 | 2 | 2025-02-15 |
| 박민수 | 3 | 2025-03-01 |
| ⋮ | ||
두 테이블에 흩어져 있던 정보가 한 줄로 붙었습니다. 여기서 문법 하나 짚고 갈게요.
members m은 “members 테이블을 m이라고 부르겠다” 는 선언이에요. 별칭 없이는
members.member_name, orders.order_id를 매번 길게 써야 합니다. 관습은
테이블 첫 글자 — m(members), o(orders), p(products).
그리고 INNER JOIN의 의미. INNER JOIN은 양쪽 다 매칭되는 행만 남깁니다. shop DB에서는 20명 중 16명이 주문했고 4명은 한 번도 안 했죠 — 그래서 결과에는 주문한 16명만 나옵니다. 반대로 만약 부모 없는 자식(FK가 깨진 상태의 주문)이 있어도 똑같이 안 나와요.
JOIN만 써도 INNER JOIN입니다. 실무 코드에선 생략형을 더 자주 봐요.
-- 두 줄은 완전히 같은 의미
... FROM members m JOIN orders o ON m.member_id = o.member_id
... FROM members m INNER JOIN orders o ON m.member_id = o.member_idLEFT JOIN — 왼쪽은 모두 보존
이번엔 요구가 바뀝니다 — “주문 안 한 회원도 보고 싶다”. INNER JOIN으로는 불가능하죠, 4명이 사라지니까. 이럴 때 두 번째 무기.
SELECT
m.member_name,
o.order_id
FROM
members m
LEFT JOIN orders o
ON m.member_id = o.member_id;| member_name | order_id |
|---|---|
| 김철수 | 1 |
| 김철수 | 6 |
| ⋮ | |
| 황태웅 | NULL |
| 안소영 | NULL |
| 류재민 | NULL |
| 백서윤 | NULL |
왼쪽 테이블(members)은 모두 보존, 오른쪽(orders)은 매칭되는 것만. 주문 안 한 회원도 결과에 나오고 — 매칭이 안 된 오른쪽 칸은 NULL로 채워집니다.
LEFT JOIN이 만들어내는 NULL은 3-7에서 다룰 함정의 씨앗입니다. 지금은 딱 하나만 기억하세요 — “매칭 안 되면 NULL”.
LEFT JOIN + IS NULL — 자식 없는 부모 찾기
방금 결과를 자세히 보면 재밌는 게 보여요. 주문 안 한 회원만 order_id가 NULL이죠? 그럼 NULL인 행만 골라내면 — “주문이 한 번도 없는 회원” 이 나옵니다.
-- 주문이 한 번도 없는 회원
SELECT
m.member_name
FROM
members m
LEFT JOIN orders o
ON m.member_id = o.member_id
WHERE
o.order_id IS NULL;| member_name |
|---|
| 황태웅 |
| 안소영 |
| 류재민 |
| 백서윤 |
LEFT JOIN + IS NULL = “자식 없는 부모 찾기”. “한 번도 주문 안 한 회원”, “리뷰 없는 상품”, “댓글 없는 게시글” — 전부 이 한 패턴입니다. 실무에서 정말 자주 쓰여요.
INNER vs LEFT — 언제 어떤 걸 쓰나
오늘 배운 걸 한 표로 정리하면 이렇게 됩니다.
| 결과에 들어가는 행 | 언제 쓰나 | |
|---|---|---|
| INNER JOIN | 양쪽 다 매칭되는 행만 | ”주문 한 회원만 보고 싶다” |
| LEFT JOIN | 왼쪽은 모두 보존 + 매칭되면 오른쪽도 (없으면 NULL) | “전체 회원 + 그 회원의 주문” |
| LEFT JOIN + IS NULL | 매칭 안 된 왼쪽만 | ”주문 안 한 회원만 보고 싶다” |
요구사항이 들어오면 먼저 물으세요 — “매칭 안 되는 쪽을 버릴 건가, 살릴 건가?” 버리면 INNER, 살리면 LEFT, 매칭 안 된 것만 보고 싶으면 LEFT + IS NULL. 이 세 패턴이 머릿속에 있으면 현실 SQL의 90%는 다 짤 수 있습니다.
3개 이상 JOIN — 체인으로 잇기
“JOIN은 두 테이블까지만 되나요?” — 아니요. 체인처럼 계속 이어 붙일 수 있습니다. 회원 → 주문 → 주문상세 → 상품, 4개 테이블을 한 번에 묶어볼게요.
-- 회원 → 주문 → 주문상세 → 상품
SELECT
m.member_name,
o.ordered_at,
p.product_name,
oi.quantity
FROM
members m
JOIN orders o
ON m.member_id = o.member_id
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN products p
ON oi.product_id = p.product_id
ORDER BY
m.member_name,
o.ordered_at;| member_name | ordered_at | product_name | quantity |
|---|---|---|---|
| 강서연 | 2025-04-20 | 라운드 티셔츠 | 2 |
| 강서연 | 2025-04-20 | 니트 | 1 |
| 강서연 | 2025-09-05 | SQL 입문서 | 1 |
| 강서연 | 2026-01-05 | 카디건 | 1 |
| ⋮ | |||
ON 조건 세 개를 보세요 — 전부 부모ID = 자식ID입니다. 회원이 부모, 주문이 자식. 주문이 부모, 주문상세가 자식. 상품이 부모, 주문상세가 자식. 관계의 흐름만 따라가면 JOIN이 몇 개든 그냥 풀려요.
핵심 원칙 — 부모-자식 관계 흐름대로 JOIN한다. 테이블 관계도가 머릿속에 그려지면 ON 조건은 고민할 게 없습니다. 이게 바로 “DB 모델링이 SQL의 출발점” 이라는 이 강의 메시지의 실증이고 — 섹션 4에서 그 모델링을 제대로 배웁니다.
자주 막히는 지점
ON 조건을 빼먹으면 모든 조합(회원 20 × 주문 40 = 800행)이 만들어집니다 — 에러가 아니라 조용히 잘못된 결과가 나오는 게 더 무서운 점이에요.
member_id가 members와 orders 양쪽에 다 있어서, “어느 쪽 member_id 말하는
거야?” 라고 MySQL이 묻는 겁니다. JOIN에서 별칭이 거의 필수인 이유.
m.member_id처럼.WHERE에 오른쪽 테이블 컬럼 조건(예: o.ordered_at 비교)을 걸면 NULL 행이
전부 걸러져서 INNER JOIN과 똑같아집니다. LEFT JOIN의 “왼쪽 보존”이 무너지는
대표 실수예요.
IS NULL 패턴만은 WHERE가 맞습니다.INNER와 LEFT, 그리고 IS NULL 패턴까지 — 오늘 배운 세 가지가 손에 익도록 연습 문제부터 풀고 가세요. 다음 강의는 이 JOIN이 데이터를 조용히 뻥튀기하는 함정 — 위 4테이블 쿼리에 GROUP BY를 그대로 얹었다가 회원별 주문 수가 이상하게 불어나는, AI가 가장 자주 틀리는 패턴입니다.