본문으로 건너뛰기

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_idusers.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, nameorder_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 집합
  • 벤 다이어그램을 보면 내부 조인(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_idnameemail
1sean@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_idorder_id
11
12
23
27
34
45

내부 조인으로 시도해보기

  • 결과를 한 번에 쉽게 확인하기 위해 내부 조인(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_idnameuser_idorder_id
111
112
2네이트23
2네이트27
3세종대왕34
4이순신45
5마리 퀴리56
  • 결과를 보면 '레오나르도 다빈치'(user_id: 6)가 결과에서 완전히 사라졌다.
  • 내부 조인(INNER JOIN)은 양쪽 테이블에 모두 존재하는 데이터만 보여주기 때문이다.
  • 즉 주문 기록이 없는 고객은 orders 테이블에 짝이 없어서 결과에서 제외된다.

4.2 외부 조인의 필요성

  • 외부 조인(OUTER JOIN)은 두 테이블을 조인할 때, 특정 테이블의 데이터는 ON 조건이 맞지 않더라도 모두 결과에 포함시키는 방법이다.
  • 이때 기준이 되는 테이블이 어느 쪽이냐에 따라 LEFT OUTER JOINRIGHT 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 JOINLEFT JOIN
    • RIGHT OUTER JOINRIGHT 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_idnameuser_idorder_id
111
112
2네이트23
2네이트27
3세종대왕34
4이순신45
5마리 퀴리56
6레오나르도 다빈치NULLNULL
  • 실행 결과를 보자. INNER JOIN에서는 보이지 않던 '레오나르도 다빈치(user_id:6)'가 드디어 나타났다.
  • 그는 주문 기록이 없기 때문에, orders 테이블에서 가져온 user_idorder_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 JOINRIGHT JOIN보다 훨씬 더 많이 사용된다.
  • 보통 분석의 기준이 되는 테이블을 FROM 절에 먼저 쓰고, 필요한 정보를 담은 다른 테이블들을 LEFT JOIN으로 하나씩 붙여나가는 방식으로 쿼리를 작성하는 것이 더 직관적이기 때문이다.
  • RIGHT JOIN은 테이블의 순서를 바꾸면 언제나 LEFT JOIN으로 동일하게 표현할 수 있다.

정리

  • LEFT JOINRIGHT JOIN은 조인할 때 기준 테이블의 위치를 정한다.
  • 사람들은 보통 위에서 아래로, 왼쪽에서 오른쪽으로 글을 읽기 때문에 기준이 되는 내용이 먼저 나오는 것을 선호한다.
  • LEFT JOIN은 기준 테이블이 FROM 절에 먼저 나오므로 더 읽기 편하다.
  • 따라서 기준 테이블을 FROM 절에 먼저 쓰는 습관을 들이는 것이 좋다.
  • FROMJOIN 절에 있는 테이블의 위치를 바꾸면 LEFT JOINRIGHT JOIN으로 변경할 수 있다.
  • 반대로, FROMJOIN 절에 있는 테이블의 위치를 바꾸면 RIGHT JOINLEFT JOIN으로 변경할 수 있다.
  • 이러한 이유로 실무에서는 대부분 LEFT JOIN을 사용하며, RIGHT JOIN은 잘 사용하지 않는다.

5. 조인의 특징

  • 두 테이블을 조인할 때 어떤 경우에는 행이 더 늘어나고 어떤 경우에는 행이 늘어나지 않고 그대로인 경우가 있다.
  • 이 부분은 데이터베이스를 다루는 데 있어 정말 중요하므로 반드시 제대로 이해해야 한다.

5.1 조인 시 행 수 변화의 원리

  • 조인은 다음과 같은 특징이 있다.
  • 기준으로 삼는 테이블의 한 행(Row)이 다른 쪽 테이블의 여러 행과 연결될 수 있다면, 결과의 전체 행 수는 늘어난다.
  • 반대로 한 행이 다른 쪽 테이블의 단 하나의 행과 연결되거나, 아무 행과도 연결되지 않는다면 행의 수는 늘어나지 않는다.
  • 그렇다면 대체 언제 행이 늘어나고, 언제 그대로일까?
  • 이 원리를 이해하려면 테이블 간의 관계, 특히 **기본 키(Primary Key, PK)**와 **외래 키(Foreign Key, FK)**의 관계를 알아야 한다.

Primary Key (PK)

  • 테이블에서 각 행을 고유하게 식별하는 값이다.
  • users 테이블의 user_idproducts 테이블의 product_id가 여기에 해당한다.
  • PK는 테이블 내에서 절대로 중복될 수 없다.

Foreign Key (FK)

  • 다른 테이블의 PK를 참조하는 값이다.
  • orders 테이블의 user_idusers 테이블의 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)

  • 우리는 지금까지 서로 다른 테이블, 즉 usersorders처럼 명확하게 구분된 대상을 연결하는 법을 배웠다.
  • 그런데 만약 연결해야 할 대상이 다른 테이블이 아니라 바로 '자기 자신'이라면 어떻게 해야 할까?
  • 실무에서는 이런 경우가 생각보다 흔하다.
  • 우리 회사 조직도를 관리하는 employees 테이블을 예로 들어보자.
  • 이 테이블에는 모든 직원의 정보가 들어있다. 그런데 각 직원의 '상사' 또한 우리 회사의 '직원'이다.
  • 즉, 상사 정보 역시 employees 테이블 안에 들어있다.

7.1 문제 상황

"각 직원의 이름과 바로 위 직속 상사의 이름을 나란히 함께 출력하려면 어떻게 해야 할까?"

SELECT * FROM employees;

실행 결과

employee_idnamemanager_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 JOININNER JOIN, OUTER JOIN처럼 새로운 종류의 JOIN 명령어가 아니다.
  • 이것은 **하나의 테이블을 자기 자신과 조인하는 '기법'**을 일컫는 말이다.
  • SQL이 이 기법을 가능하게 하는 핵심 원리는 바로 **테이블 별칭(Alias)**에 있다.
  • 하나의 테이블에 서로 다른 별칭을 두 개 부여함으로써, 데이터베이스가 이들을 마치 다른 두 개의 테이블인 것처럼 인식하게 만드는 것이다.
  • 우리는 employees 테이블을 두 개 복사해서 하나는 직원을 나타내는 e (employee)로, 다른 하나는 상사를 나타내는 m (manager)으로 사용한다고 상상하면 이해하기 쉽다.
    • e 테이블: 모든 직원의 목록
    • m 테이블: 모든 상사의 목록 (실체는 똑같은 employees 테이블)
  • 그리고 e 테이블의 manager_idm 테이블의 employee_id가 같은 것들을 연결(JOIN)해주면, 우리는 직원의 이름(e.name)과 상사의 이름(m.name)을 한 줄에 나란히 놓을 수 있게 된다.

7.3 실습: 직원-상사 목록 만들기

  • 이제 SELF JOIN 기법을 사용해서 직원과 상사 목록을 만드는 쿼리를 작성해 보자.

1단계: INNER JOIN을 이용한 SELF JOIN

  • 가장 기본적인 INNER JOIN으로 두 테이블 em을 연결한다.
  • 연결 조건은 "직원의 매니저 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_namemanager_name
박사장김회장
이부장박사장
최과장이부장
정대리최과장
홍사원최과장
  • 결과를 보면, 각 직원 옆에 직속 상사의 이름이 정확히 출력된 것을 확인할 수 있다.
  • 그런데 한 가지 이상한 점이 있다. 전체 직원 중 '김회장'이 employee_name 목록에 보이지 않는다. 왜일까?
  • 그의 manager_idNULL이기 때문이다.
  • INNER JOINON 조건이 맞는, 즉 e.manager_id에 값이 있는 데이터만 결과에 포함시키므로 manager_idNULL인 '김회장'은 조인 대상에서 제외된 것이다.

2단계: LEFT JOIN을 이용한 SELF JOIN

  • 만약 상사가 없는 최상위 리더, 즉 '김회장'까지 포함한 전체 직원 목록을 보고 싶다면 어떻게 해야 할까?
  • 이럴 때 바로 LEFT JOIN이 필요하다.
  • 직원을 나타내는 e 테이블을 기준으로 삼고, 상사 정보를 왼쪽에 붙이는 것이다.
  • 상사 정보가 없으면(manager_idNULL이면) 그 자리는 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_namemanager_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;

실행 결과

sizecolor
SRed
SBlue
SBlack
MRed
MBlue
MBlack
LRed
LBlue
LBlack
XLRed
XLBlue
XLBlack
  • 보다시피 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_namesizecolor
기본티셔츠-Red-SSRed
기본티셔츠-Blue-SSBlue
기본티셔츠-Black-SSBlack
기본티셔츠-Red-MMRed
기본티셔츠-Blue-MMBlue
기본티셔츠-Black-MMBlack
기본티셔츠-Red-LLRed
기본티셔츠-Blue-LLBlue
기본티셔츠-Black-LLBlack
기본티셔츠-Red-XLXLRed
기본티셔츠-Blue-XLXLBlue
기본티셔츠-Black-XLXLBlack
  • 이 결과를 새로운 테이블에 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 ... SELECTSELECT 문으로 조회된 결과를 즉시 다른 테이블에 삽입하는 기능이다.
  • 이 방법을 사용하면 대량의 초기 데이터를 효율적으로 구축하거나, 기존 데이터를 가공하여 새로운 테이블에 저장하는 등의 작업을 할 수 있다.

결과 확인

SELECT * FROM product_options;

실행 결과

option_idproduct_namesizecolor
1기본티셔츠-Red-SSRed
2기본티셔츠-Blue-SSBlue
3기본티셔츠-Black-SSBlack
4기본티셔츠-Red-MMRed
5기본티셔츠-Blue-MMBlue
6기본티셔츠-Black-MMBlack
7기본티셔츠-Red-LLRed
8기본티셔츠-Blue-LLBlue
9기본티셔츠-Black-LLBlack
10기본티셔츠-Red-XLXLRed
11기본티셔츠-Blue-XLXLBlue
12기본티셔츠-Black-XLXLBlack
  • CROSS JOIN으로 생성한 12개의 상품 옵션 조합이 product_options 테이블에 성공적으로 삽입된 것을 확인할 수 있다.
  • 이처럼 CROSS JOIN은 단순히 데이터를 조합하는 것을 넘어, 새로운 데이터 세트를 만들거나 초기 시스템을 구축할 때 유용하게 활용될 수 있다.
  • 특히 상품의 다양한 옵션(사이즈, 색상, 재질 등)을 조합하여 마스터 데이터를 생성하는 시나리오에서 좋은 도구가 된다.

8.6 실무에서의 치명적인 주의사항

  • CROSS JOIN은 모든 경우의 수를 만들어주기 때문에 유용하지만, 실무에서는 아주 신중하게 사용해야 하는, 어떻게 보면 가장 위험한 조인이기도 하다.
  • 왜냐하면 결과의 행 수가 기하급수적으로 늘어날 수 있기 때문이다.
  • 만약 당신이 실수로 100만 건의 데이터가 있는 users 테이블과 10만 건의 데이터가 있는 products 테이블을 CROSS JOIN한다면 어떻게 될까?
  • 결과는 100만 * 10만 = 1000억 건이 된다.
  • 이 쿼리를 실행하는 순간 데이터베이스 서버는 아마 응답을 멈추거나 다운될 것이다.