[DB] JOIN은 쉬운놈이다

wannabeing·2025년 11월 14일

CS

목록 보기
11/12
post-thumbnail

JOIN이 필요한 이유 (Feat. 정규화)

만약 JOIN을 사용하지 않고 하나의 테이블에 모든 데이터를 저장하면 어떤 문제가 생길까?
어떤 문제들이 있는지 알아보자.

1. 데이터 중복 (Redundancy)

A고객이 상품을 100개를 주문한다고 하면,
A고객의 결제내용뿐만 아니라 개인정보(이메일, 닉네임 등)도 100번 중복되어 저장된다.
따라서 데이터 중복으로 인한 저장공간 낭비가 발생하게 된다.

2. 갱신 이상 (Update Anomaly)

A고객이 배송주소를 변경하게 된다면 A고객의 모든 데이터의 배송주소를 변경해야 한다.
하나라도 누락되게 된다면 이전주소로 배송되게되는 대참사가 일어날 것이다.
어느 배송주소가 변경된 주소인지도 헷갈리게 된다.
따라서 데이터의 일관성이 깨지게 된다.

3. 삽입 이상 (Insertion Anomaly)

하나의 테이블에 모든 데이터가 저장되어 있기 때문에
새로운 상품을 등록하고자 하면, 새로운 상품에 대한 주문이 없기 때문에 상품등록을 할 수 없게 된다.
따라서 데이터 추가에 대한 오류가 발생한다.

4. 삭제 이상 (Deletion Anomaly)

B고객은 상품을 1번 주문한 기록이 있다.
어쩌다가 해당 데이터를 삭제해야하게 되었다.
상품데이터를 삭제하고자 했지만, B고객의 모든 데이터도 함께 삭제되게 된다.
소중한 B고객님을 잃게 되는 것이다.
따라서 데이터 삭제에 대한 오류가 발생한다.

이러한 문제들 때문에 우리는 정규화(Normalization) 과정을 거치게 된다.
정규화는 데이터의 일관성을 해치는 위와 같은 문제점들을 해결하기 위해 데이터를 논리적인 단위로 분리하는 작업이다.
즉, 데이터를 분리하는 이유(정규화)는 데이터를 잘 관리하기 위함이다.

이때, 통합된 보고서를 만들고자 할 때 특정 기준으로 다시 데이터를 합쳐야한다.
해당 기술이 바로 JOIN(조인)이다.


이제 우리는 왜 조인(JOIN)이 필요한지 알게 되었다.
조인은 내부조인과 외부조인으로 나누어진다.
내부조인 먼저 알아보자.


내부조인 (INNER JOIN)

양쪽 테이블에 공통으로 존재하는 데이터만 결과로 보여주는 기능이다.

1. 내부조인 문법

SELECT 컬럼1, 컬럼2 ...
FROM 테이블A
JOIN 테이블B
ON 테이블A.연결컬럼 = 테이블B.연결컬럼;
  • 내부조인은 INNER 키워드를 생략할 수 있다.
  • ON절에서 참(true)이 되는 값들만 보여준다.

2. 내부조인 작동순서

SELECT
	users.user_id,
    users.user_name,
    orders.order_date
FROM orders
JOIN users
ON users.user_id = orders.user_id
WHERE orders.status = 'COMPLETED';

위 쿼리문의 논리적인 작동순서를 알아보자.

1) FROM / JOIN
가장 먼저 ON절에 명시된 조건을 만족하는 행들을 결합하여 통합 가상테이블을 생성한다.
예시쿼리에서 users.user_id = orders.user_id가 이에 해당한다.

2) WHERE
가상테이블에서 WHERE절의 조건을 만족하는 행들만 필터링된다.
예시쿼리에서는 status가 COMPLETED인 행들이 이에 해당한다.

3) SELECT
마지막으로 SELECT절에 명시된 행들만 추출하여 반환한다.
예시쿼리에서 user_id, user_name, order_date가 이에 해당한다.

## 내부조인 작동순서 (논리)
FROM/JOIN (테이블결합) → WHERE (필터링) → SELECT (컬럼추출 및 반환)

💡 쿼리 최적화기 (Query Optimizer)

DB 내부에서는 논리적인 순서와 상관없이 쿼리를 더 효율적인 순서로 실행한다.
예를들어, COMPLETED 상태의 행들만 먼저 필터링하고 남은 행들에 대하여 조인을 실행하게 된다면 성능 최적화가 될 수 있다.

쿼리 최적화기때문에 실제 물리적인 실행순서는 달라질 수 있지만,
최종결과는 논리적인 순서와 동일하다.


3. 내부조인 순서는 중요할까?

내부조인은 교집합을 찾는 것과 같기 때문에 A에서 B를 합치나, B에서 A를 합치나
결과는 똑같다. 그렇다면 내부조인의 순서는 중요할까?

✨ 가독성 증가 (논리적인 흐름)

  • 주문목록 중심으로 고객정보를 조회하고 싶다면 FROM orders JOIN users
  • 고객목록 중심으로 주문정보를 조회하고 싶다면 FROM users JOIN orders

외부조인 (OUTER JOIN)

더 나아가서, 존재하는 유저지만 한번도 주문하지 않는 고객과 같은
교집합이 아닌 데이터들을 추출하고 싶을 때가 있다.

이럴 때 외부조인이 필요하다.
외부조인은 교집합 밖의 영역(OUTER)을 포함한다는 의미다.

외부조인(OUTER JOIN)은 두 테이블을 조인할 때,
두 테이블 중에 하나의 테이블의 데이터는 ON 조건에 맞지 않아도
모두 결과에 포함시키는 조인 방법이다.

외부조인 문법

SELECT *
FROM users u
LEFT JOIN orders o
ON u.order_id = o.order_id
WHERE u.order_id IS NULL
ORDER BY o.order_id;

JOIN 절에서 LEFT/RIGHT를 입력하면 된다.

  • 존재하는 유저인데, 한번도 상품주문을 안한 유저들을 추출하는 문법이다.
    LEFT JOIN이므로, users 테이블이 기준이 된다.
  • LEFT는 FROM절의 테이블(왼쪽)이 기준이 된다.
  • RIGHT는 JOIN절의 테이블(오른쪽)이 기준이 된다.

💡 실무에서는 LEFT JOIN이 자주 사용된다.
기준이 되는 테이블을 보통 먼저(FROM절에) 적기 때문이다.


셀프조인

위와같은 테이블(employees)이 존재한다고 할 때,
최과장의 매니저는 employee_id가 2인 사람이다.
매니저의 이름을 알려면 다시 테이블을 조회해서 이부장임을 알 수 있다.

이처럼 한 테이블 안에서 조회하고자 하는 컬럼(manager_id)이 같은 테이블의
다른 컬럼(employee_id)를 참조하는 구조를 '자기 참조 관계'라고 한다.

이 때 사용하는 조인이 셀프조인이다.

개념과 원리

셀프조인은 새로운 조인기법이 아니라, 하나의 테이블을 자기 자신과 조인하는 기법을 말한다.

이 기법의 핵심원리는 '테이블 별칭(Alias)'에 있다.
하나의 테이블에 다른 별칭을 부여함으로써 DB가 두개의 테이블로 인식하게 만드는 것이다.

  • e 테이블: 모든 직원의 목록
  • m 테이블: 모든 상사의 목록

셀프조인 문법

SELECT
	e.name as employee_name,
    m.name as manger_name
FROM employees e # 직원 목록
JOIN employees m # 상사 목록
ON e.manager_id = m.employee_id;

manager_id가 NULL인 경우에는 데이터에 포함되지 않는다.
따라서 NULL인 데이터를 포함하기 위해 외부조인을 하자.

셀프조인을 외부조인으로

SELECT
	e.name as employee_name,
    m.name as manger_name
FROM employees e
LEFT JOIN employees m # LEFT 외부조인
ON e.manager_id = m.emplyee_id;

manger_id가 NULL인 데이터까지 나오는 것을 확인하였다.


CROSS 조인

모든 조인은 ON이라는 명령어를 통해 이미 존재하는 데이터들의 관계를
조합하여 찾아내는 작업이었다.

만약, 애초에 짝이 없거나 존재하지 않는 데이터들을 갖고 관계를 조합해야된다면?
예시를 들자면, "컬러는 Red, Blue, Green, Black, 사이즈는 S, M, L, XL로 이루어진 티셔츠가 있다.
S이면서 Red인 티셔츠 .. 와 같은 데이터를 보여주고 싶다면?
이럴 때 사용할 수 있는게 CROSS 조인이다.

개념과 특징

가장 단순한 조인방식으로, 한쪽 테이블의 모든행과 다른 테이블의 모든행을 연결하는 방식이다.

해당결과를 카테시안 곱(Cartesian Product)라고 부른다.

A테이블의 m개의 행, B테이블의 n개의 행이 있다고 할 때
두 테이블의 조인결과는 m*n개의 행을 갖게된다.

사용 문법

SELECT
	s.size,
    c.color
FROM sizes s
CROSS JOIN colors c;
sizecolor
SRed
SBlue
SBlack
......

위와 같은 형식으로 데이터가 출력되는걸 확인할 수 있다.

새로운 테이블 생성데이터로 INSERT까지

SELECT
	CONCAT('티셔츠-', c.color, '-', s.size) AS product_name,
	s.size,
	c.color
FROM sizes s
CROSS JOIN colors c;
product_namesizecolor
티셔츠-Red-SSRed
티셔츠-Red-MMRed
티셔츠-Red-LLRed
티셔츠-Red-XLXLRed
티셔츠-Blue-SSBlue
.........

위와같은 데이터를 새로운 테이블 생성데이터로 넣을 때,
INSERT INTO ... SELECT문을 사용할 수 있다.

INSERT INTO product_options (product_name, size, color)
SELECT
	CONCAT('티셔츠-', c.color, '-', s.size) AS product_name,
	s.size,
	c.color
FROM sizes s
CROSS JOIN colors c;

위와 같은 INSERT INTO ... SELECT문으로 우리가 만들어낸 데이터를
product_options 테이블에 데이터로 삽입하였다.

CROSS 조인 주의사항

모든 경우의 수를 간편하게 만들어주기 때문에 유용할 수 있겠지만
결과의 행이 기하급수로 늘어날 수 있기 때문에 조심해야한다.

A테이블의 100만건의 행, B테이블의 10만건의 행이 있다고 하면
100만건*10만건 = 1000억건의 데이터의 쿼리를 실행하게 된다면
서버가 멈추게 될 수도 있을 것이다. 따라서 조심해서 사용해야 한다.


조인의 특징들

조인할 때, 데이터(row/행)가 늘어나는 경우

두 테이블의 관계에서 차이가 있다.
주문(Orders)테이블과 유저(Users)테이블을 비교해보자면,
한명의 유저는 여러개의 주문을 할 수 있고, 한개의 주문에는 한명의 유저만 존재한다.

따라서 유저테이블은 PK가 존재하는 부모테이블, 주문테이블은 FK를 통해 참조하는 자식테이블이라고 할 수 있다.

  • 부모 테이블: PK를 갖고 있는 테이블 (Users)
  • 자식 테이블: FK로 부모를 참조하는 테이블 (Orders)

1. 자식 → 부모 조인 (FK→PK): 행 개수가 늘어나지 않는다.

  • 자식은 반드시 단 하나의 부모와 연결된다.
  • 따라서 PK방향으로 참조하는 경우, 행 개수는 늘어나지 않는다.

2. 부모 → 자식 조인 (PK→FK): 행 개수가 늘어날 수 있다.

  • 부모는 여러명의 자식과 연결된다.
  • 따라서 FK방향으로 참조하는 경우, 행 개수는 늘어날 수 있다.

  • 자식(Orders) → 부모(Users)로 조인하는 경우
  • SELECT * FROM Orders WHERE user_id = 1;는 2개이다.
  • user_id = 1인 유저의 주문데이터(조인X) 개수와
    조인데이터(그림) 개수는 같다.

  • 부모(Users) → 자식(Orders)로 조인하는 경우
  • SELECT * FROM Users WHERE user_id = 1;는 1개이다.
  • user_id = 1인 유저의 데이터 개수는 1개이지만,
    조인데이터(그림) 개수는 2개로 다르다.
profile
wannabe---ing

0개의 댓글