DataBase

[SQL] 2. 조인(JOIN)

sihanni 2025. 8. 14. 14:56

 

JOIN 이란?

JOIN은 두 개 이상의 테이블을 연결하여 관련된 데이터를 조합할 때 사용하는 개념을 나타낸다.

 

JOIN이 필요한 이유

관계형 데이터베이스는 정규화를 통해 데이터를 여러 테이블에 나눠 저장한다.

users 테이블에는 유저 정보만, orders에는 주문 정보만 있을때 데이터의 중복 최소화와 데이터 정합성의 유지가 가능한데, 데이터가 여러 테이블에 나뉘어져 있기 때문에 필요한 데이터를 확인하기 위해서는 조회 시 여러 테이블의 데이터를 묶어서 봐야 하는 경우가 많다.

이 때 JOIN을 사용하여 공통 키(보통 PK-FK 관계)를 기준으로 테이블을 결합하게 된다.

 

JOIN의 개념

JOIN은 두 개 이상의 테이블을 논리적으로 결합하여 하나의 결과 집합으로 만드는 연산이다.

조인 조건은 일반적으로 동일 값 비교( ON t1.col = t2.col )형태이며, 다양한 종류의 조인 유형이 존재한다.

 

JOIN의 종류

1) INNER JOIN

두 테이블에서 조건이 일치하는 행만 반환한다. 매칭되지 않으면 제외시킨다.

SELECT o.id, u.name
FROM orders o
INNER JOIN users u ON o.user_id = u.id;

 

2) LEFT OUTER JOIN

왼쪽 테이블 모든 행 + 오른쪽 매칭 행, 오른쪽에 없는 데이터는 NULL

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

 

3) SELF JOIN

자기 자신과 조인 (계층 구조/ 상위-하위 관계 조회)

SELECT e.name AS emp, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

 

4) SEMI JOIN

왼쪽 테이블에서 오른쪽 조건에 매칭되는 행만 반환, 오른쪽 컬럼은 결과에 포함되지 않음

내부적으로 EXISTS, IN 연산자를 사용

특징 : 매칭된 오른쪽 행이 여러 개이더라도 왼쪽 행은 1번만 출력

SELECT u.id, u.name
FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE o.user_id = u.id
);

 

LEFT JOIN과 햇갈릴 수 있는데 표로 둘을 비교해보면 아래와 같다.

구분 LEFT JOIN SEMI JOIN
의미 왼쪽 테이블 모든 행을 반환, 오른쪽 매칭시 결합 없으면 NULL 왼쪽 테이블에서 오른쪽 조건에 매칭되는 행만 반환
오른쪽 컬럼 결과에 포함됨 결과에 포함되지 않음
중복 행 오른쪽에 여러 매칭이 있으면 중복 발생 매칭이 여러건이어도 왼쪽 행은 1번만 출력
매칭 안된 행 NULL 로 포함 포함 안함

 

5) ANTI JOIN

왼쪽 테이블에서 오른쪽 조건에 매칭되지 않는 행만 반환

NOT EXISTS, NOT IN으로 구현

SELECT u.id, u.name
FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.user_id = u.id
);

위는 주문이 없는 사용자를 찾는 ANTI JOIN 쿼리이다. 그런데 이건 JOIN을 통해서도 구현이 아래와 같이 가능한데,

-- LEFT JOIN + NULL 체크
SELECT u.id, u.name
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;

이렇게 구현하더라도 옵티마이저가 내부적으로 ANTI JOIN 알고리즘을 사용해서 구현하기도하고,

LEFT JOIN 방식은 일단 두 테이블을 합친 후 NULL 필터를 하게 되는데, 조인 과정에서 불필요한 행 결합이 발생한다.

ANTI JOIN은 조건에서 매칭 없음을 바로 검사하니, 메모리와 네트워크 비용을 줄일 수 있다.

그리고 NOT EXISTS는 매칭되는 데이터가 없는 경우라는 의미가 직관적이라는 장점도 있다.

JOIN 동작 원리

DB 옵티마이저는 통계 정보를 기반으로 예상 비용을 계산하고, 가장 비용이 낮은 방식을 선택하게 된다.

비용 계산에 반영되는 주요 요소는 다음과 같다.

  • 조인 키 컬럼의 인덱스 유무
    • 인덱스가 있으면 Nested Loop Join이 유리
    • 인덱스가 없고 데이터가 크면 Merge Join 또는 Hash Join 고려
  • 두 테이블의 예상 행 수(Cardinality)
    • 한쪽이 매우 작고(수백 건), 다른 쪽이 크면 Nested Loop Join이 적합
    • 둘 다 대량(수백만 건 이상)이고 정렬 가능하면 Merge Join,
      정렬이 없으면 Hash Join
  • 정렬 상태
    • 이미 조인 키 기준으로 정렬되어 있으면 Merge Join 비용↓
    • 정렬이 안 돼 있으면 Merge Join은 정렬 비용 때문에 불리
  • 메모리 사용 가능량
    • 메모리가 충분하면 대량 데이터에 Hash Join을 써도 효율적
    • 메모리가 작으면 디스크 스필(Spill) 위험 → Nested Loop나 Merge Join 선호

 

DB 옵티마이저는 조인 연산을 다음 방식으로 수행한다.

1. Nested Loop Join

  • 한쪽 테이블 행을 하나씩 읽고, 다른 테이블에서 매칭 검색
  • 소규모 + 인덱스 효율적일 때 유리함
  • 조건 : 한쪽 테이블이 작고, 조인 조건 컬럼에 인덱스가 있을 때
  • 동작 : 작은 테이블의 행을 하나씩 읽으면서, 큰 테이블에서 인덱스 검색

2. Merge Join

  • 두 테이블이 조인 키 기준으로 정렬된 상태에서 병합
  • 정렬 비용이 들지만 대량 처리에 유리
  • 조건 : 양쪽이 대량 데이터고, 조인 키 기준으로 정렬이 가능하거나 정렬되어 있고, 해시 테이블 생성보다 정렬 비용이 더 싸다고 판단
  • 동작 : 두 테이블을 조인 키 기준으로 정렬 후 병합

3. Hash Join

  • 한 테이블을 해시 테이블로 만들고, 다른 테이블의 행을 해시 탐색
  • MySQL 8.0.18+, PostgreSQL, Oracle 등에서 사용 가능
  • 조건 : 양쪽이 대량 데이터고, 정렬이 안되어있고, 인덱스도 없고 메모리가 충분할 때
  • 동작 : 한쪽 테이블을 해시 테이블로 만들고, 다른 쪽의 행을 해시 검색 

해시 테이블

key-value 쌍을 저장하는 자료구조이다. 데이터를 배열처럼 저장하면서 키를 해시 함수에 넣어서 배열 인덱스를 계산한다.

평균적으로 O(1)시간에 원하는 값을 찾을 수 있다.

  • 저장(Store)
    • 키를 해시 함수에 넣어 → 해시 값(정수)을 얻음
    • 해시 값을 배열 크기로 나눈 나머지를 인덱스로 사용
    • 해당 인덱스 위치에 데이터를 저장
  • 검색(Lookup)
    • 찾고 싶은 키를 해시 함수에 넣어 같은 방식으로 인덱스 계산
    • 해당 인덱스 위치에서 값 읽기
옵티마이저 판단 요약
인덱스 O + 한쪽 작음 → Nested Loop
인덱스 X + 정렬 O → Merge Join
인덱스 X + 정렬 X → Hash Join