Join
1. 조인이 필요한 이유
- 왜 불편하게 데이터를 여러 테이블에 나누어 저장하는 걸까?
- 차라리 처음부터 하나의 큰 테이블에 주문 정보, 고객 정보, 상품 정보를 모두 담아두면 편하지 않았을까?
2. 만약 모든 데이터를 하나의 테이블에 저장하면?
- 궁금증을 해결하기 위해,
users,products,orders테이블을 모두 제거하고, 이 테이블의 필드를 하나로 합친all_in_one테이블을 상상해 보자. - 당장은 편해 보인다. 하지만 이런 방식은 실무에서 재앙을 불러온다.
- 다음과 같은 심각한 문제들이 발생하기 때문이다.
2.1 데이터베이스 이상 현상들
데이터 중복 (Redundancy)
- '션' 고객이 상품을 100번 주문했다고 생각해 보자.
- 그의 이름, 이메일, 주소 정보가 100번이나 불필요하게 반복 저장된다.
- 이건 매우 큰 저장 공간의 낭비다.
갱신 이상 (Update Anomaly)
- 만약 '션' 고객이 이메일 주소를 변경했다고 가정해 보자.
- 우리는 '션'이 주문한 100개의 주문 데이터를 모두 찾아서, 이메일 정보를 일일이 새로운 주소로 변경해야 한다.
- 만약 실수로 단 하나라도 누락한다면? 어떤 주문에서는 고객의 이메일이 예전 주소로, 다른 주문에서는 새 주소로 저장되어 데이터의 일관성이 깨져버린다.
- 어떤 정보가 진짜인지 믿을 수 없게 되는 것이다.
삽입 이상 (Insertion Anomaly)
- 우리 쇼핑몰에 아직 아무도 주문하지 않은 새로운 상품 '초경량 노트북'을 등록하고 싶다.
- 하지만 이 테이블 구조에서는 '주문'이 발생해야만 데이터를 추가할 수 있다.
- 주문한 사람이 없으니, 상품 정보조차 등록할 수 없는 상황이 발생한다.
삭제 이상 (Deletion Anomaly)
- '이순신' 고객이 딱 한 번 주문한 기록이 있다고 하자.
- 만약 회사 정책상 이 주문 기록을 삭제해야 한다면 어떻게 될까?
- 주문 데이터를 삭제하는 순간, '이순신' 고객의 이름, 이메일, 주소 정보까지 데이터베이스에서 영원히 사라져 버릴 수 있다.
- 우리는 단지 주문 내역 하나를 지웠을 뿐인데, 소중한 고객 정보까지 잃게 되는 것이다.
정규화의 필요성
- 이러한 문제들 때문에 우리는 데이터베이스를 설계할 때 **정규화(Normalization)**라는 과정을 거친다.
- 정규화는 데이터의 중복을 최소화하고, 데이터의 일관성을 해치는 '이상 현상'들을 방지하기 위해 데이터를 논리적인 단위로 분리하는 과정이다.
- 우리가
users,products,orders로 테이블을 나눈 것이 바로 이 정규화의 결과물이다. - 데이터베이스 정규화 참고
2.2 그래서 조인이 필요하다
- 이제 우리는 왜 데이터를 분리해서 저장하는지 이해했다.
- 데이터의 중복을 막고, 일관성을 지키기 위해서다. 즉, 데이터를 '잘 관리하기 위해서'다.
- 하지만 잘 관리하기 위해 흩어놓은 데이터에서 의미 있는 정보를 얻으려면, 이 흩어진 조각들을 다시 합쳐야만 한다.
- "어떤 고객이 어떤 상품을 주문했는지"와 같은 통합된 보고서를 만들기 위해, 분리된 테이블들을 다시 연결해야 하는 것이다.
이때 사용하는 기술이 바로 조인(JOIN)이다.
- 조인은 두 개 이상의 테이블을 특정 컬럼을 기준으로 연결하여, 마치 처음부터 하나의 테이블이었던 것처럼 보여주는 기능이다.
- 보통 테이블을 설계할 때 연결고리로 사용하는
기본 키(Primary Key)와외래 키(Foreign Key)를 사용해 이들을 합친다. orders테이블의user_id외래 키(FK)는users테이블의user_id기본 키(PK)와 연결된다.orders테이블의product_id외래 키(FK)는products테이블의product_id기본 키(PK)와 연결된다.- 조인은 데이터 정규화(분리)를 통해 얻는 일관성과 효율성의 장점은 그대로 유지하면서, 우리가 원하는 통합된 정보를 얻을 수 있게 해주는, 데이터 분리와 통합을 완성하는 기술이다.
3. 내부 조인 (INNER JOIN)
- 데이터의 일관성과 효율성을 위해 테이블을 분리(정규화)하며, 이렇게 분리 된 테이블을 다시 합쳐 의미 있는 정보를 얻기 위해 조인(
JOIN)이 필요하다는 사실을 깨달았다.
3.1 내부 조인의 개념
- 내부 조인(
INNER JOIN)은 두 테이블을 연결할 때, 양쪽 테이블에 모두 공통으로 존재하는 데이터만을 결과로 보여준다. - 기준이 되는 컬럼(예:
orders.user_id와users.user_id)의 값이 서로 일치하는 행들만 짝을 지어주는 것이다.
3.2 내부 조인 문법
SELECT 컬럼1, 컬럼2, ...
FROM 테이블A
INNER JOIN 테이블B
ON 테이블A.연결컬럼 = 테이블B.연결컬럼;
FROM: 기준이 되는 첫 번째 테이블을 지정한다.INNER JOIN: 연결할 두 번째 테이블을 지정한다.ON: 조인에서 가장 중요한 부분이다. 두 테이블을 어떤 조건으로 연결할지 명시하는 연결고리다.ON절의 조건이 참(true)이 되는 행들만 결과에 포함된다.
3.3 조인 작동 순서
- 결제 완료된 주문의 고객명과 주문 날짜가 어떤 순서로 조회된 것인지 조인이 작동하는 논리적인 순서를 알아보자.
SELECT
users.user_id,
users.name,
orders.order_date
FROM orders
INNER JOIN users ON orders.user_id = users.user_id
WHERE orders.status = 'COMPLETED';
- 데이터베이스는 이 쿼리를 다음과 같은 논리적인 순서로 처리한다.
1. FROM / JOIN
- 가장 먼저
FROM절의orders테이블과INNER JOIN으로 연결된users테이블을 연결하기 위해ON절에 명시된orders.user_id = users.user_id조건을 만족하는 행들을 결합하여 하나의 큰 가상 테이블을 생성한다.
2. WHERE
JOIN을 통해 생성된 가상 테이블에서WHERE절의 조건인orders.status = 'COMPLETED'를 만족하는 행들만 필터링한다.- 즉,
COMPLETED상태의 주문 데이터만 남게 된다.
3. SELECT
- 마지막으로, 필터링된 결과에서
SELECT절에 명시된users.user_id,name과order_date컬럼을 추출하여 최종 결과를 반환한다.
논리적 처리 순서 요약
JOIN을 통해 두 테이블을 먼저 합친 가상의 테이블을 만든 후,WHERE절의 조건에 따라 필요한 행을 걸러낸다.- 그리고 최종적으로 원하는 필드를
SELECT로 선택하는 순서로 작동한다. FROM/JOIN (테이블 결합)→WHERE (조건 필터링)→SELECT (컬럼 선택)
3.4 쿼리 최적화기 (Query Optimizer)
- 사용자와 데이터베이스 간의 논리적 순서는 일종의 약속이지만, 데이터베이스 내부의 **쿼리 최적화기(Query Optimizer)**는 쿼리를 더 효율적인 방식으로 실행한다.
- 예를 들어,
orders테이블에서COMPLETED상태의 행만 먼저 선택한 다음, 남은 행을 기준으로users테이블과 조인하는 방식으로 진행하면 조인 대상이 줄어들어 성능이 더 최적화될 수 있다. - 쿼리 최적화기를 통해 실제 물리적인 실행 순서는 달라질 수 있지만 어떻게 작동하든 최종 결과는 논리적인 순서와 동일하다.
- 하지만 데이터베이스의 최적화 방식을 잘 이해하면 같은 결과를 얻으면서도 조회 성능을 최적화할 수 있다.
3.5 조인과 집합
- 내부 조인을 이해하는 또 다른 방법은 바로 '집합'의 관점에서 바라보는 것이다.
- 학창 시절 수학 시간에 배운 벤 다이어그램을 떠올리면 아주 쉽게 이해할 수 있다.
- 어려운 수학을 이야기하는 것이 아니다! 단순한 동그라미 그리기 관점으로 이해하자!
- 내부 조인(
INNER JOIN)은 두 테이블의 교집합을 찾는 것과 같다. - 두 집합(테이블)에서 공통된 원소(연결 컬럼의 값이 일치하는 데이터)만을 결과로 반환한다.
- A 집합:
orders테이블에 있는 모든 사용자들의user_id집합 - B 집합:
users테이블에 있는 모든user_id집합
- A 집합:
- 벤 다이어그램을 보면 내부 조인(
INNER JOIN)의 이름을 왜 내부라고 지었는지 알 수 있다.- 내부 조인은 벤 다이어그램에서 둘의 겹친 영역인 교집합 영역을 말 한다.
- 교집합 영역은 벤 다이어그램에서 내부에 있는 데이터를 뜻한다.
- 이후에 설명할 외부 조인(
OUTER JOIN)은 교집합 영역의 밖(OUTER)의 행까지 포함한다는 의미이다.
- 결론적으로,
INNER JOIN은 어느 한쪽에만 데이터가 존재하는 경우는 결과에 포함시키지 않고, 양쪽 모두에 명확하게 연결고리가 있는 데이터만을 짝지어 보여준다.
3.6 내부 조인과 조인 방향
- 내부 조인은 양방향이다.
- A 테이블과 B 테이블이 있다고 하면, A → B로 조인할 수 있다면 반대로 B → A로 조인할 수 있다.
- 그리고 그 결과는 항상 동일하다.
왜 결과가 동일할까?
- 내부 조인은 두 테이블 간의 교집합을 찾는 연산이기 때문이다.
A와 B의 교집합과B와 A의 교집합이 같은 것과 같은 원리다.
실무 팁: 조인 순서는 언제 중요할까?
- 내부 조인에서는 결과가 같으므로 어떤 순서로 작성해도 무방하다.
- 하지만 쿼리를 읽는 사람의 입장에서 어떤 데이터가 중심이 되는가에 따라 순서를 정하면 가독성이 높아진다.
- 주문 목록을 중심으로 고객 정보를 추가하고 싶다면
FROM orders JOIN users - 고객 목록을 중심으로 주문 정보를 조회하고 싶다면
FROM users JOIN orders
- 주문 목록을 중심으로 고객 정보를 추가하고 싶다면
3.7 내부 조인의 한계
- 그런데 여기서 또 다른 질문이 생긴다.
- "그렇다면, 우리 쇼핑몰에 가입은 했지만 아직 한 번도 주문하지 않은 고객은 어떻게 찾아낼 수 있을까?"
- 내부 조인(
INNER JOIN)은 양쪽에 모두 데이터가 있는 경우만 보여주기 때문에 이 질문에는 답할 수 없다. - 이 문제를 해결하기 위해 외부 조인(
OUTER JOIN)에 대해 알아보겠다.
4. 외부 조인 (OUTER JOIN)
- 내부 조인(
INNER JOIN)을 통해 우리는 양쪽 테이블에 모두 존재하는, 짝이 맞는 데이터들을 성공적으로 연결했다. - 하지만 실무에서는 종종 짝이 없는, 소외된 데이터를 찾아야 할 때가 있다.
- 위에서 던졌던 질문을 다시 떠올려 보자.
- "우리 쇼핑몰에 가입은 했지만, 아직 한 번도 주문하지 않은 고객은 누구일까?"
- "야심차게 출시했지만, 아직 단 한 번도 팔리지 않은 비운의 상품은 무엇일까?"
- 이 질문들에 내부 조인은 답할 수 없다.
- 왜냐하면 내부 조인은 주문 기록이 있는 고객, 판매 기록이 있는 상품, 즉
orders테이블과 짝이 맞는 데이터만 보여주기 때문이다. - 주문 한 적 없는 고객이나 팔린 적 없는 상품은
orders테이블에 짝이 없으므로 결과에서 아예 누락된다.
4.1 내부 조인의 한계 확인
users 테이블 조회
SELECT user_id, name, email FROM users;
실행 결과
| user_id | name | |
|---|---|---|
| 1 | 션 | sean@example.com |
| 2 | 네이트 | nate@example.com |
| 3 | 세종대왕 | sejong@example.com |
| 4 | 이순신 | sunsin@example.com |
| 5 | 마리 퀴리 | marie@example.com |
| 6 | 레오나르도 다빈치 | vinci@example.com |
orders 테이블 조회
SELECT user_id, order_id FROM orders
ORDER BY user_id;
실행 결과
| user_id | order_id |
|---|---|
| 1 | 1 |
| 1 | 2 |
| 2 | 3 |
| 2 | 7 |
| 3 | 4 |
| 4 | 5 |
내부 조인으로 시도해보기
- 결과를 한 번에 쉽게 확인하기 위해 내부 조인(
INNER JOIN)을 사용해서 고객과 주문을 연결해보자.
SELECT
u.user_id,
u.name,
o.user_id,
o.order_id
FROM users u
JOIN orders o ON u.user_id = o.user_id
ORDER BY u.user_id;
INNER는 생략했다.INNER를 생략하고JOIN만 적으면 내부 조인(INNER JOIN)으로 작동한다.- 조인 과정을 보면 레오나르도 다빈치의
users.user_id:6에 해당하는orders.user_id:6이 없다. - 따라서 레오나르도 다빈치는 조인 대상에서 제외된다.
실행 결과
| user_id | name | user_id | order_id |
|---|---|---|---|
| 1 | 션 | 1 | 1 |
| 1 | 션 | 1 | 2 |
| 2 | 네이트 | 2 | 3 |
| 2 | 네이트 | 2 | 7 |
| 3 | 세종대왕 | 3 | 4 |
| 4 | 이순신 | 4 | 5 |
| 5 | 마리 퀴리 | 5 | 6 |
- 결과를 보면 '레오나르도 다빈치'(
user_id: 6)가 결과에서 완전히 사라졌다. - 내부 조인(
INNER JOIN)은 양쪽 테이블에 모두 존재하는 데이터만 보여주기 때문이다. - 즉 주문 기록이 없는 고객은
orders테이블에 짝이 없어서 결과에서 제외된다.
4.2 외부 조인의 필요성
- 외부 조인(
OUTER JOIN)은 두 테이블을 조인할 때, 특정 테이블의 데이터는ON조건이 맞지 않더라도 모두 결과에 포함시키는 방법이다. - 이때 기준이 되는 테이블이 어느 쪽이냐에 따라
LEFT OUTER JOIN과RIGHT OUTER JOIN으로 나뉜다. - 교집합 영역은 물론이고, 기준이 되는 테이블의 데이터는 결과에 모두 포함된다.
- '레오나르도 다빈치'(
user_id: 6)를 결과에 포함하려면LEFT OUTER JOIN을 선택하면 된다.
풀 외부 조인(FULL OUTER JOIN)
- 양쪽 모두를 함께 포함하는 풀 외부 조인이라는 방법도 있다.
- 실무에서 잘 사용하지 않고, MySQL은 지원하지 않는다.
4.3 LEFT JOIN vs RIGHT JOIN
- 외부 조인의 핵심은 '기준 테이블'을 정하는 것이다.
LEFT OUTER JOIN은 왼쪽 테이블을,RIGHT OUTER JOIN은 오른쪽 테이블을 기준으로 삼는다.
OUTER는 생략 가능
- 다음과 같이 OUTER는 생략해서 사용할 수 있다. 실무에서는 생략하는 것을 권장한다.
LEFT OUTER JOIN→LEFT JOINRIGHT OUTER JOIN→RIGHT JOIN
LEFT JOIN (또는 LEFT OUTER JOIN)
LEFT JOIN구문의 왼쪽(FROM 절)에 있는 테이블이 기준이 된다.- 일단 왼 쪽 테이블의 모든 데이터를 결과에 포함시킨다.
- 그다음,
ON조건에 맞는 데이터를 오른쪽 테이블에서 찾아 옆에 붙여준다. - 만약 오른쪽 테이블에 짝이 맞는 데이터가 없다면, 그 자리는
NULL값으로 채워진다.
RIGHT JOIN (또는 RIGHT OUTER JOIN)
RIGHT JOIN구문의 오른쪽(JOIN 절)에 있는 테이블이 기준이 된다.- 일단 오른쪽 테이블의 모든 데이터를 결과에 포함시킨다.
- 그다음,
ON조건에 맞는 데이터를 왼쪽 테이블에서 찾아 붙인다. - 마찬가지로 왼쪽 테이블에 짝이 맞는 데이터가 없다면, 그 자리는
NULL로 채워진다.
4.4 LEFT JOIN으로 '한 번도 주문하지 않은 고객' 찾기
- 첫 번째 질문, "한 번도 주문하지 않은 고객은 누구인가?"에 답을 찾아보자.
- 이 질문의 기준은 '고객'이다. 따라서
users테이블이LEFT JOIN의 왼쪽에 위치해야 한다.
1단계: users를 기준으로 orders 테이블 LEFT JOIN 하기
- 먼저
users테이블의 모든 고객을 일단 다 보여주고, 오른쪽에 주문 정보를 붙여보자.
SELECT
u.user_id,
u.name,
o.user_id,
o.order_id
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
ORDER BY u.user_id;
LEFT JOIN을 사용했다.users가 왼쪽이고,orders가 오른쪽이다.- SQL을 보면 FROM 다음에
users가 먼저 나오고 그 다음에orders가 나온다.
실행 결과
| user_id | name | user_id | order_id |
|---|---|---|---|
| 1 | 션 | 1 | 1 |
| 1 | 션 | 1 | 2 |
| 2 | 네이트 | 2 | 3 |
| 2 | 네이트 | 2 | 7 |
| 3 | 세종대왕 | 3 | 4 |
| 4 | 이순신 | 4 | 5 |
| 5 | 마 리 퀴리 | 5 | 6 |
| 6 | 레오나르도 다빈치 | NULL | NULL |
- 실행 결과를 보자.
INNER JOIN에서는 보이지 않던 '레오나르도 다빈치(user_id:6)'가 드디어 나타났다. - 그는 주문 기록이 없기 때문에,
orders테이블에서 가져온user_id와order_id컬럼의 값이 모두NULL로 채워져 있다. - 이것이 바로
OUTER JOIN이다.
2단계: NULL인 데이터만 필터링하기
- 주문 정보가
NULL인 고객이 바로 '한 번도 주문하지 않은 고객'이다. WHERE절을 사용해서 이들만 찾아보자.
SELECT
u.user_id,
u.name,
u.email
FROM users AS u
LEFT JOIN orders AS o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
NULL 비교
NULL은 '값이 없음'을 나타내는 특별한 상태이므로,=연산자로 비교할 수 없다.- 반드시
IS NULL또는IS NOT NULL을 사용해야 한다.
4.5 LEFT JOIN vs RIGHT JOIN 실무 사용
- 실무에서는
LEFT JOIN이RIGHT JOIN보다 훨씬 더 많이 사용된다. - 보통 분석의 기준이 되는 테이블을
FROM절에 먼저 쓰고, 필요한 정보를 담은 다른 테이블들을LEFT JOIN으로 하나씩 붙여나가는 방식으로 쿼리를 작성하는 것이 더 직관적이기 때문이다. RIGHT JOIN은 테이블의 순서를 바꾸면 언제나LEFT JOIN으로 동일하게 표현할 수 있다.
정리
LEFT JOIN과RIGHT JOIN은 조인할 때 기준 테이블의 위치를 정한다.- 사람들은 보통 위에서 아래로, 왼쪽에서 오른쪽으로 글을 읽기 때문에 기준이 되는 내용이 먼저 나오는 것을 선호한다.
LEFT JOIN은 기준 테이블이FROM절에 먼저 나오므로 더 읽기 편하다.- 따라서 기준 테이블을
FROM절에 먼저 쓰는 습관을 들이는 것이 좋다. FROM과JOIN절에 있는 테이블의 위치를 바꾸면LEFT JOIN을RIGHT JOIN으로 변경할 수 있다.- 반대로,
FROM과JOIN절에 있는 테이블의 위치를 바꾸면RIGHT JOIN을LEFT JOIN으로 변경할 수 있다. - 이러한 이유로 실무에서는 대부분
LEFT JOIN을 사용하며,RIGHT JOIN은 잘 사용하지 않는다.
5. 조인의 특징
- 두 테이블을 조인할 때 어떤 경우에는 행이 더 늘어나고 어떤 경우에는 행이 늘어나지 않고 그대로인 경우가 있다.
- 이 부분은 데이터베이스를 다루는 데 있어 정말 중요하므로 반드시 제대로 이해해야 한다.
5.1 조인 시 행 수 변화의 원리
- 조인은 다음과 같은 특징이 있다.
- 기준으로 삼는 테이블의 한 행(Row)이 다른 쪽 테이블의 여러 행과 연결될 수 있다면, 결과의 전체 행 수는 늘어난다.
- 반대로 한 행이 다른 쪽 테이블의 단 하나의 행과 연결되거나, 아무 행과도 연결되지 않는다면 행의 수는 늘어나지 않는다.
- 그렇다면 대체 언제 행이 늘어나고, 언제 그대로일까?
- 이 원리를 이해하려면 테이블 간의 관계, 특히 **기본 키(Primary Key, PK)**와 **외래 키(Foreign Key, FK)**의 관계를 알아야 한다.
Primary Key (PK)
- 테이블에서 각 행을 고유하게 식별하는 값이다.
users테이블의user_id나products테이블의product_id가 여기에 해당한다.- PK는 테이블 내에서 절대로 중복될 수 없다.
Foreign Key (FK)
- 다른 테이블의 PK를 참조하는 값이다.
orders테이블의user_id는users테이블의user_id를 참조하는 FK다.- FK는 참조하는 테이블에서 여러 번 중복되어 나타날 수 있다.
- 예를 들어, 한 명의 고객(
user_id)이 여러 번의 주문(orders)을 할 수 있기 때문이다.
5.2 부모-자식 관계
- 이 관계를 '부모-자식 관계'에 비유하면 이해하기 쉽다.
- 부모 테이블 (Parent Table): PK를 가지고 있는 테이블. (
users,products) - 자식 테이블 (Child Table): FK를 통해 부모 테이블을 참조하는 테이블. (
orders)
5.3 조인 시 데이터 행 수 변화 규칙
1. 자식 → 부모 조인 (FK → PK 참조): 행 개수가 늘어나지 않는다
orders테이블을 기준으로users테이블을 조인하는 경우다.- 자식 테이블(
orders)의 각 주문 정보는 반드시 **단 한 명의 부모(users)**하고만 연결된다. - 주문 하나가 여러 고객의 것일 수는 없기 때문이다.
- 따라서 기준 테이블인
orders의 행 개수가 그대로 유지된다. - PK는 유일한 하나의 값만 저장된다. 따라서 PK 방향으로 참조하는 경우 행 개수가 늘어나지 않는다.
2. 부모 → 자식 조인 (PK → FK 참조): 행 개수가 늘어날 수 있다
users테이블을 기준으로orders테이블을 조인하는 경우다.- 부모 테이블(
users)의 한 고객은 **여러 명의 자식(여러 건의orders)**을 가질 수 있다. - 이 경우, 한 고객의 정보를 여러 주문 정보에 각각 매칭시켜야 하므로, 고객 정보 행이 주문 건수만큼 복제되어 전체 행의 수가 늘어난다.
- FK는 같은 값을 여러개 저장할 수 있다. 따라서 FK 방향으로 참조하는 경우 행 개수가 늘어날 수 있다.
5.4 정리: 언제 행이 늘어나고 언제 그대로인가?
- 조인 시 결과 행의 수가 변하는지 여부는 두 테이블의 관계와 어떤 테이블을 기준으로 삼는지에 달려있다.
행 개수 유지: 자식에서 부모로 조인할 때 (to-One)
- FK → PK 조인
- 방향:
FROM 자식 테이블 JOIN 부모 테이블(예:FROM orders JOIN users) - 원리: 자식 테이블의 모든 행은 부모 테이블의 단 하나의 행과 매칭된다. (주문은 반드시 한 명의 고객에게 속한다.)
- 결과: 기준이 되는 자식 테이블의 행 개수가 그대로 유지된다.
orders테이블이 7행이면 결과도 7행이다.
행 개수 증가 가능: 부모에서 자식으로 조인할 때 (to-Many)
- PK → FK 조인
- 방향:
FROM 부모 테이블 JOIN 자식 테이블(예:FROM users JOIN orders) - 원리: 부모 테이블의 한 행은 자식 테이블의 여러 행과 매칭될 수 있다. (한 명의 고객이 여러 번 주문할 수 있다.)
- 결과: 부모 행이 자식 행의 개수만큼 복제되면서 전체 행의 수가 기준 테이블보다 늘어날 수 있다. 주문을 2번 한 고객은 결과 테이블에서 2개의 행을 차지하게 된다.
5.5 실무에서 이것이 왜 중요할까?
- 이 원리를 모르면 데이터를 잘못 분석하게 될 위험이 크다.
- 예를 들어, 모든 고객과 그들의 주문 정보를 보기 위해
FROM users JOIN orders를 수행했다고 하자. - 이 결과에서 고객 수를 세기 위해
COUNT(u.user_id)를 실행하면 어떻게 될까? - 전체 고객 수인 6이 나올까? 아니다. 주문을 여러 번 한 고객이 중복 계산되므로, 전체 주문 수인 7이 나온다.
- 이처럼 조인으로 인해 데이터가 어떻게 변하는지 정확히 이해해야만, 합계(
SUM), 평균(AVG), 개수(COUNT) 같은 집계 함수를 올바르게 사용하고 원하는 분석 결과를 정확하게 도출할 수 있다. - 쿼리를 작성하기 전에 항상 어떤 테이블을 기준으로 삼을지, 그리고 조인으로 인해 행 수가 증가하는 상황인지 아닌지, 먼저 생각하는 습관을 들이는 것이 중요하다.
6. 조인의 유연성
- 데이터베이스에서 조인(JOIN)은 주로 기본 키(PK)와 외래 키(FK)를 써서 테이블을 연결하는 것이 가장 일반적이고 중요한 방식이다.
- 그러나 조인의 핵심 원리는 '두 테이블의 특정 열(column)의 값이 같은가?' 이기에, 실제로는 어떤 열이든 조인 조건으로 쓸 수 있다.
- 데이터 타입만 같다면 말이다.
- PK-FK 관계가 아니더라도 여러 상황에서 조인을 활용할 수 있다.
6.1 다양한 조인 활용 예시
동명이인 찾기 (이름으로 조인)
- 고객 테이블과 직원 테이블에 모두 '이름' 열이 있다면, 이 두 테이블을 이름으로 조인하여 고객과 직원의 이름이 같은 경우를 찾아낸다.
SELECT A.이름, A.연락처, B.부서
FROM 고객 AS A
JOIN 직원 AS B ON A.이름 = B.이름;
특정 날짜의 이벤트 연결 (날짜로 조인)
- 주문 테이블의 '주문일자'와 마케팅 이벤트 테이블의 '이벤트_시작일'을 기준으로 조인하여, 특정 이벤트가 있던 날에 들어온 주문을 분석할 수 있다.
SELECT A.주문번호, A.주문금액, B.이벤트명
FROM 주문 AS A
JOIN 마케팅_이벤트 AS B ON A.주문일자 = B.이벤트_시작일;
지역별 데이터 분석 (지역 코드로 조인)
- 매장 테이블의 '우편번호' 앞 두 자리와 지역별_인구통계 테이블의 '지역코드'를 연결하여, 매장이 있는 지역의 인구 통계 데이터를 함께 분석한다.
SELECT A.매장명, B.평균소득
FROM 매장 AS A
JOIN 지역별_인구통계 AS B ON SUBSTRING(A.우편번호, 1, 2) = B.지역코드;
로그 데이터 분석 (상태 코드로 조인)
- 웹서버 로그 테이블의 '상태코드'와 에러_코드_정의 테이블의 '코드'를 조인하여, 로그에 기록된 에러가 무엇을 뜻하는지 바로 파악할 수 있다.
SELECT A.요청URL, B.에러설명
FROM 웹서버_로그 AS A
JOIN 에러_코드_정의 AS B ON A.상태코드 = B.코드;
6.2 PK-FK 조인이 중요한 이유
- 이처럼 조인은 매우 유연하지만, 실무에서는 데이터의 정확성과 일관성을 위해 대부분 PK와 FK를 쓴다.
- 이름처럼 중복될 수 있거나, 언제든 바뀔 수 있는 값으로 조인하면 데이터가 엉뚱하게 연결될 위험이 크기 때문이다.
7. 셀프 조인 (SELF JOIN)
- 우리는 지금까지 서로 다른 테이블, 즉
users와orders처럼 명확하게 구분된 대상을 연결하는 법을 배웠다. - 그런데 만약 연결해야 할 대상이 다른 테이블이 아니라 바로 '자기 자신'이라면 어떻게 해야 할까?
- 실무에서는 이런 경우가 생각보다 흔하다.
- 우리 회사 조직도를 관리하는
employees테이블을 예로 들어보자. - 이 테이블에는 모든 직원의 정보가 들어있다. 그런데 각 직원의 '상사' 또한 우리 회사의 '직원'이다.
- 즉, 상사 정보 역시
employees테이블 안에 들어있다.
7.1 문제 상황
"각 직원의 이름과 바로 위 직속 상사의 이름을 나란히 함께 출력하려면 어떻게 해야 할까?"
SELECT * FROM employees;
실행 결과
| employee_id | name | manager_id |
|---|---|---|
| 1 | 김회장 | NULL |
| 2 | 박사장 | 1 |
| 3 | 이부장 | 2 |
| 4 | 최과장 | 3 |
| 5 | 정대리 | 4 |
| 6 | 홍사원 | 4 |
- '홍사원'(
employee_id: 6)의manager_id는 4다. - 이 상사의 이름을 알려면, 우리는 다시 이 테이블에서
employee_id가 4인 직원을 찾아야 한다. - '최과장'이라는 것을 알 수 있다.
- 이처럼 한 테이블 안에서 자신의 컬럼(
manager_id)이 같은 테이블의 다른 컬럼(employee_id)을 참조하는 구조를 '자기 참조 관계'라고 한다. - 이런 구조의 데이터를 한 번의 쿼리로 조회하기 위해 사용하는 기술이 바로
SELF JOIN이다.
7.2 SELF JOIN의 개념과 원리
SELF JOIN은INNER JOIN,OUTER JOIN처럼 새로운 종류의JOIN명령어가 아니다.- 이것은 **하나의 테이블을 자기 자신과 조인하는 '기법'**을 일컫는 말이다.
- SQL이 이 기법을 가능하게 하는 핵심 원리는 바로 **테이블 별칭(Alias)**에 있다.
- 하나의 테이블에 서로 다른 별칭을 두 개 부여함으로써, 데이터베이스가 이들을 마치 다른 두 개의 테이블인 것처럼 인식하게 만드는 것이다.
- 우리는
employees테이블을 두 개 복사해서 하나는 직원을 나타내는e(employee)로, 다른 하나는 상사를 나타내는m(manager)으로 사용한다고 상상하면 이해하기 쉽다.e테이블: 모든 직원의 목록m테이블: 모든 상사의 목록 (실체는 똑같은employees테이블)
- 그리고
e테이블의manager_id와m테이블의employee_id가 같은 것들을 연결(JOIN)해주면, 우리는 직원의 이름(e.name)과 상사의 이름(m.name)을 한 줄에 나란히 놓을 수 있게 된다.
7.3 실습: 직원-상사 목록 만들기
- 이제
SELF JOIN기법을 사용해서 직원과 상사 목록을 만드는 쿼리를 작성해 보자.
1단계: INNER JOIN을 이용한 SELF JOIN
- 가장 기본적인
INNER JOIN으로 두 테이블e와m을 연결한다. - 연결 조건은 "직원의 매니저 ID(
e.manager_id)가 상사의 직원 ID(m.employee_id)와 같을 때"이다.
SELECT
e.name AS employee_name,
m.name AS manager_name
FROM
employees e
JOIN
employees m ON e.manager_id = m.employee_id;
e.name은 직원의 이름,m.name은 상사의 이름이 된다.- 컬럼에도
AS를 사용해 별칭을 붙여주면 결과를 이해하기가 훨씬 수월하다. (물론AS는 생략할 수 있다.)
실행 결과
| employee_name | manager_name |
|---|---|
| 박사장 | 김회장 |
| 이부장 | 박사장 |
| 최과장 | 이부장 |
| 정대리 | 최과장 |
| 홍사원 | 최과장 |
- 결과를 보면, 각 직원 옆에 직속 상사의 이름이 정확히 출력된 것을 확인할 수 있다.
- 그런데 한 가지 이상한 점이 있다. 전체 직원 중 '김회장'이
employee_name목록에 보이지 않는다. 왜일까? - 그의
manager_id는NULL이기 때문이다. INNER JOIN은ON조건이 맞는, 즉e.manager_id에 값이 있는 데이터만 결과에 포함시키므로manager_id가NULL인 '김회장'은 조인 대상에서 제외된 것이다.
2단계: LEFT JOIN을 이용한 SELF JOIN
- 만약 상사가 없는 최상위 리더, 즉 '김회장'까지 포함한 전체 직원 목록을 보고 싶다면 어떻게 해야 할까?
- 이럴 때 바로
LEFT JOIN이 필요하다. - 직원을 나타내는
e테이블을 기준으로 삼고, 상사 정보를 왼쪽에 붙이는 것이다. - 상사 정보가 없으면(
manager_id가NULL이면) 그 자리는NULL로 표시될 것이다.
SELECT
e.name AS employee_name,
m.name AS manager_name
FROM
employees AS e
LEFT JOIN
employees AS m ON e.manager_id = m.employee_id;
실행 결과
| employee_name | manager_name |
|---|---|
| 김회장 | NULL |
| 박사장 | 김회장 |
| 이부장 | 박사장 |
| 최과장 | 이부장 |
| 정대리 | 최과장 |
| 홍사원 | 최과장 |
- 이제
LEFT JOIN을 통해 상사가 없는 '김회장'까지 포함된 완벽한 조직도 리스트를 만들었다. - 이처럼
SELF JOIN은 테이블 별칭을 활용하여 자기 참조 관계를 풀어내는 유용한 기법이다. - 조직도뿐만 아니라 웹사이트의 카테고리와 서브카테고리, 게시판의 원본 글과 답변 글 같은 계층형 데이터를 다룰 때 반드시 필요한 기술이니 기억해두자.
8. 크로스 조인 (CROSS JOIN)
- 지금까지 우리가 배운
INNER,OUTER,SELF조인은 모두ON이라는 연결고리를 통해 테이블에 '이미 존재하는' 관계를 찾아내는 작업이었다. - 즉, 특정 조건에 맞는 짝을 찾는 것이 핵심이었다.
- 그런데 만약, 애초에 짝이나 관계가 없는 두 집단을 가지고 가능한 모든 조합을 만들어내야 한다면 어떻게 해야 할까?
8.1 문제 상황
"우리 쇼핑 몰에서 새로운 기본 티셔츠를 판매하려고 한다. 사이즈는 S, M, L, XL 네 종류이고, 색상은 Red, Blue, Black 세 종류이다. 판매를 시작하기 전에, 재고 관리를 위해 가능한 모든 사이즈와 색상 조합을 담은 상품 마스터 데이터를 미리 생성하고 싶다."
- 이 업무는 'S 사이즈이면서 Red 색상인 상품', 'M 사이즈이면서 Red 색상인 상품' ... 'XL 사이즈이면서 Black 색상인 상품' 까지, 생각할 수 있는 모든 경우의 수를 목록으로 만들어야 한다.
sizes테이블과colors테이블 사이에는 미리 정해진 연결고리(ON조건)가 없다.- 우리는 이 둘을 강제로 조합해야 한다.
- 이럴 때 사용하는 조인이 바로
CROSS JOIN이다.
8.2 CROSS JOIN의 개념과 카테시안 곱 (Cartesian Product)
CROSS JOIN은 조인 조건 없이, 한쪽 테이블의 모든 행을 다른 쪽 테이블의 모든 행과 하나씩 전부 연결하는, 가장 단순한 조인 방식이다.- 연결고리가 없기 때문에
ON절을 사용하지 않는다. CROSS JOIN의 결과를 수학 용어로 카테시안 곱(Cartesian Product) 또는 데카르트 곱이라고 부른다.- 만약 A 테이블에
m개의 행이 있고, B 테이블에n개의 행이 있다면, 두 테이블을CROSS JOIN한 결과 는m * n개의 행을 갖게 된다. - 우리 예시의
sizes테이블은 4개의 행을,colors테이블은 3개의 행을 가지고 있다. - 따라서 두 테이블을
CROSS JOIN하면4 * 3 = 12개의 행으로 이루어진 결과가 나올 것이다.
8.3 CROSS JOIN 실습
sizes 테이블 조회
SELECT * FROM sizes;
실행 결과
| size |
|---|
| S |
| M |
| L |
| XL |
colors 테이블 조회
SELECT * FROM colors;
실행 결과
| color |
|---|
| Black |
| Blue |
| Red |
CROSS JOIN 실행
SELECT
s.size,
c.color
FROM
sizes s
CROSS JOIN
colors c;
실행 결과
| size | color |
|---|---|
| S | Red |
| S | Blue |
| S | Black |
| M | Red |
| M | Blue |
| M | Black |
| L | Red |
| L | Blue |
| L | Black |
| XL | Red |
| XL | Blue |
| XL | Black |
- 보다시피
sizes테이블의 각 행이colors테이블의 모든 행과 하나씩 짝을 이뤄 총 4 x 3, 12개의 완벽한 조합이 만들어졌다.
8.4 상품명 자동 생성
- 여기서 한 걸음 더 나아가, 이 조합을 이용해 상품명을 자동으로 생성해 볼 수도 있다.
- 문자열을 합치는
CONCAT함수를 활용하면 된다.
SELECT
CONCAT('기본티셔츠-', c.color, '-', s.size) AS product_name,
s.size,
c.color
FROM
sizes AS s
CROSS JOIN
colors AS c;
실행 결과
| product_name | size | color |
|---|---|---|
| 기본티셔츠-Red-S | S | Red |
| 기본티셔츠-Blue-S | S | Blue |
| 기본티셔츠-Black-S | S | Black |
| 기본티셔츠-Red-M | M | Red |
| 기본티셔츠-Blue-M | M | Blue |
| 기본티셔츠-Black-M | M | Black |
| 기본티셔츠-Red-L | L | Red |
| 기본티셔츠-Blue-L | L | Blue |
| 기본티셔츠-Black-L | L | Black |
| 기본티셔츠-Red-XL | XL | Red |
| 기본티셔츠-Blue-XL | XL | Blue |
| 기본티셔츠-Black-XL | XL | Black |
- 이 결과를 새로운 테이블에
INSERT하면, 상품 마스터 데이터를 손쉽게 구축할 수 있다.
8.5 INSERT INTO ... SELECT로 상품 옵션 마스터 테이블 만들기
- 이제
CROSS JOIN으로 만든 상품 옵션 조합을 새로운 테이블에 저장해 보자. - 이렇게 데이터를 한 번에 대량으로 삽입할 때
INSERT INTO ... SELECT구문을 사용한다. - 우리는 티셔츠 상품에 대한 모든 사이즈와 색상 조합을 저장할
product_options테이블을 만들 것이다. - 이 테이블은
option_id,product_name,size,color필드를 가진다.
product_options 테이블 생성
CREATE TABLE product_options (
option_id BIGINT AUTO_INCREMENT,
product_name VARCHAR(255) NOT NULL,
size VARCHAR(10) NOT NULL,
color VARCHAR(20) NOT NULL,
PRIMARY KEY (option_id)
);
CROSS JOIN 결과를 테이블에 삽입
INSERT INTO product_options (product_name, size, color)
SELECT
CONCAT('기본티셔츠-', c.color, '-', s.size) AS product_name,
s.size,
c.color
FROM
sizes AS s
CROSS JOIN
colors AS c;
INSERT INTO ... SELECT는SELECT문으로 조회된 결과를 즉시 다른 테이블에 삽입하는 기능이다.- 이 방법을 사용하면 대량의 초기 데이터를 효율적으로 구축하거나, 기존 데이터를 가공하여 새로운 테이블에 저장하는 등의 작업을 할 수 있다.
결과 확인
SELECT * FROM product_options;
실행 결과
| option_id | product_name | size | color |
|---|---|---|---|
| 1 | 기본티셔츠-Red-S | S | Red |
| 2 | 기본티셔츠-Blue-S | S | Blue |
| 3 | 기본티셔츠-Black-S | S | Black |
| 4 | 기본티셔츠-Red-M | M | Red |
| 5 | 기본티셔츠-Blue-M | M | Blue |
| 6 | 기본티셔츠-Black-M | M | Black |
| 7 | 기본티셔츠-Red-L | L | Red |
| 8 | 기본티셔츠-Blue-L | L | Blue |
| 9 | 기본티셔츠-Black-L | L | Black |
| 10 | 기본티셔츠-Red-XL | XL | Red |
| 11 | 기본티셔츠-Blue-XL | XL | Blue |
| 12 | 기본티셔츠-Black-XL | XL | Black |
CROSS JOIN으로 생성한 12개의 상품 옵션 조합이product_options테이블에 성공적으로 삽입된 것을 확인할 수 있다.- 이처럼
CROSS JOIN은 단순히 데이터를 조합하는 것을 넘어, 새로운 데이터 세트를 만들거나 초기 시스템을 구축할 때 유용하게 활용될 수 있다. - 특히 상품의 다양한 옵션(사이즈, 색상, 재질 등)을 조합하여 마스터 데이터를 생성하는 시나리오에서 좋은 도구가 된다.
8.6 실무에서의 치명적인 주의사항
CROSS JOIN은 모든 경우의 수를 만들어주기 때문에 유용하지만, 실무에서는 아주 신중하게 사용해야 하는, 어떻게 보면 가장 위험한 조인이기도 하다.- 왜냐하면 결과의 행 수가 기하급수적으로 늘어날 수 있기 때문이다.
- 만약 당신이 실수로 100만 건의 데이터가 있는
users테이블과 10만 건의 데이터가 있는products테이블을CROSS JOIN한다면 어떻게 될까? - 결과는
100만 * 10만 = 1000억건이 된다. - 이 쿼리를 실행하는 순간 데이터베이스 서버는 아마 응답을 멈추거나 다운될 것이다.