SQL 기초문법 정리 - JOIN

SQL 기초문법 정리 - JOIN

JOIN이란?

JOIN은 두 개 이상의 테이블을 연결하여 관련된 데이터를 함께 조회하는 기능이다.
관계형 데이터베이스에서는 데이터 정규화를 통해 중복을 제거하고 여러 테이블로 분할하여 저장한다.


테이블 별칭과 기본 문법

테이블 별칭 사용

JOIN을 사용할 때는 테이블 별칭을 사용하는 것이 가독성과 유지보수에 좋다.

1
2
3
4
5
6
7
8
9
-- 별칭 없이 (길고 복잡함)
SELECT users.name, orders.order_date, orders.total_amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;

-- 별칭 사용 (간결하고 명확함)
SELECT u.name, o.order_date, o.total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

JOIN의 기본 구조

1
2
3
4
5
SELECT 컬럼목록
FROM 테이블1 별칭1
[JOIN타입] 테이블2 별칭2 ON 조인조건
WHERE 필터조건
ORDER BY 정렬조건;

JOIN의 종류

1. INNER JOIN (내부 조인)

양쪽 테이블에서 조인 조건을 만족하는 행만 반환한다. 가장 일반적으로 사용되는 JOIN 타입.

1
2
3
4
5
6
7
-- 실제로 주문한 고객과 주문 정보만 조회
SELECT u.name AS 고객명,
       o.order_date AS 주문일,
       o.total_amount AS 주문금액,
       o.status AS 상태
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

결과: 주문이 있는 고객만 나타남

1
2
3
4
5
고객명  | 주문일     | 주문금액 | 상태
김철수  | 2023-06-01 | 1200000  | completed
이영희  | 2023-06-02 | 50000    | completed
김철수  | 2023-06-03 | 300000   | pending
박민수  | 2023-06-04 | 150000   | shipped

2. LEFT JOIN (왼쪽 외부 조인)

왼쪽 테이블의 모든 행과 오른쪽 테이블의 일치하는 행을 반환한다.
오른쪽 테이블에 일치하는 데이터가 없으면 NULL로 표시.

1
2
3
4
5
6
7
8
-- 모든 사용자와 주문 정보 (주문하지 않은 사용자도 포함)
SELECT u.name AS 고객명,
       u.city AS 도시,
       o.order_date AS 주문일,
       o.total_amount AS 주문금액
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.name;

결과: 모든 사용자가 나타나며, 주문 없는 사용자는 주문 정보가 NULL

1
2
3
4
5
6
고객명  | 도시 | 주문일     | 주문금액
김철수  | 서울 | 2023-06-01 | 1200000
김철수  | 서울 | 2023-06-03 | 300000
박민수  | 대구 | 2023-06-04 | 150000
이영희  | 부산 | 2023-06-02 | 50000
정수현  | 인천 | NULL       | NULL     ← 주문이 없는 사용자

3. RIGHT JOIN (오른쪽 외부 조인)

오른쪽 테이블의 모든 행과 왼쪽 테이블의 일치하는 행을 반환한다.
실무에서는 LEFT JOIN으로 테이블 순서를 바꿔서 사용.

1
2
3
4
5
6
7
8
9
-- RIGHT JOIN (권장하지 않음)
SELECT u.name, o.order_date, o.total_amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

-- 위와 동일한 결과를 LEFT JOIN으로 (권장)
SELECT u.name, o.order_date, o.total_amount
FROM orders o
LEFT JOIN users u ON o.user_id = u.id;

4. FULL OUTER JOIN (완전 외부 조인)

양쪽 테이블의 모든 행을 반환한다. 어느 한쪽에만 존재하는 데이터도 모두 포함.

1
2
3
4
5
6
-- 모든 사용자와 모든 주문 정보
SELECT u.name AS 고객명,
       o.order_date AS 주문일,
       o.total_amount AS 주문금액
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;

복합 테이블 JOIN

실무에서는 3개 이상의 테이블을 조인하는 경우가 많다.

예제 테이블 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 카테고리 테이블
categories:
id | name     | description
1  | 전자제품 | IT 관련 전자제품
2  | 도서     | 각종 서적
3  | 의류     | , 신발, 액세서리

-- 상품 테이블
products:
id | name        | category_id | price   | stock
1  | 노트북      | 1           | 1200000 | 50
2  | SQL 입문서  | 2           | 30000   | 100
3  | 티셔츠      | 3           | 25000   | 200

-- 주문 상세 테이블
order_items:
id | order_id | product_id | quantity | unit_price
1  | 1        | 1          | 1        | 1200000
2  | 2        | 3          | 2        | 25000
3  | 3        | 2          | 1        | 30000

다중 테이블 JOIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 주문 상세 정보: 고객 → 주문 → 주문상품 → 상품 → 카테고리
SELECT u.name AS 고객명,
       o.order_date AS 주문일,
       c.name AS 카테고리,
       p.name AS 상품명,
       oi.quantity AS 수량,
       oi.unit_price AS 단가,
       oi.quantity * oi.unit_price AS 소계
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id
INNER JOIN categories c ON p.category_id = c.id
ORDER BY o.order_date, u.name;

JOIN 순서의 중요성

1
2
3
4
5
6
7
8
-- 효율적인 JOIN 순서: 작은 테이블부터 시작
SELECT ...
FROM categories c                    -- 1. 가장 작은 테이블
INNER JOIN products p ON c.id = p.category_id
INNER JOIN order_items oi ON p.id = oi.product_id
INNER JOIN orders o ON oi.order_id = o.id
INNER JOIN users u ON o.user_id = u.id
WHERE c.name = '전자제품'           -- 조건을 먼저 적용

조인 조건과 필터 조건

ON절 vs WHERE절

1
2
3
4
5
6
7
8
-- ON절: 조인 조건 (테이블을 어떻게 연결할 것인가)
-- WHERE절: 필터 조건 (연결된 결과에서 어떤 행을 선택할 것인가)
SELECT u.name, o.order_date, o.total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id          -- 조인 조건
WHERE o.total_amount > 100000                     -- 필터 조건
  AND o.order_date >= '2023-06-01'               -- 필터 조건
  AND u.city = '서울';                           -- 필터 조건

복합 조인 조건

1
2
3
4
5
6
-- 여러 조건을 동시에 만족해야 하는 경우
SELECT u.name, o.order_date, o.total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id 
                   AND o.status = 'completed'     -- 조인 시점에 필터링
                   AND u.city = '서울';           -- 조인 시점에 필터링

LEFT JOIN에서 ON vs WHERE 차이

1
2
3
4
5
6
7
8
9
10
11
-- ON절에 조건: 조인 전에 필터링 (권장)
SELECT u.name, o.order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id 
                  AND o.status = 'completed';    -- 완료된 주문만 조인

-- WHERE절에 조건: 조인 후에 필터링 (INNER JOIN과 같은 효과)
SELECT u.name, o.order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed';                    -- 결과적으로 INNER JOIN

집계와 함께 사용하는 JOIN

그룹별 집계

1
2
3
4
5
6
7
8
9
10
11
-- 사용자별 주문 통계 (주문하지 않은 사용자도 포함)
SELECT u.name AS 고객명,
       u.city AS 도시,
       COUNT(o.id) AS 주문횟수,
       COALESCE(SUM(o.total_amount), 0) AS 총주문금액,
       COALESCE(AVG(o.total_amount), 0) AS 평균주문금액,
       MAX(o.order_date) AS 최근주문일
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name, u.city
ORDER BY 총주문금액 DESC;

카테고리별 판매 현황

1
2
3
4
5
6
7
8
9
10
11
-- 카테고리별 상품 수와 판매 통계
SELECT c.name AS 카테고리,
       COUNT(DISTINCT p.id) AS 상품수,
       COUNT(oi.id) AS 주문건수,
       COALESCE(SUM(oi.quantity), 0) AS 총판매량,
       COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS 총매출
FROM categories c
LEFT JOIN products p ON c.id = p.category_id
LEFT JOIN order_items oi ON p.id = oi.product_id
GROUP BY c.id, c.name
ORDER BY 총매출 DESC;
This post is licensed under CC BY 4.0 by the author.