테이블 합치기: JOIN

고객, 주문, 상품처럼 서로 다른 대상은 보통 테이블을 나눠 저장합니다. JOIN은 키로 연결된 테이블을 한 번의 조회에서 함께 읽는 방법입니다.

기본 조인

주문에는 customer_id가, 고객에는 이름이 있습니다. 둘을 함께 보려면 두 테이블이 어떻게 맞물리는지 알려줘야 합니다.

1

SELECT o.order_number,
       o.ordered_at,
       o.total,
       c.first_name || ' ' || c.last_name AS customer,
       c.country
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.ordered_at DESC
LIMIT 20;
Planche에서 실행

결과 — 20행 중 처음 5행

order_numberordered_attotalcustomercountry
ORD-2025-0024802025-12-28 23:52:146333.77Elena MeyerCA
ORD-2025-0005052025-12-28 11:16:392276.51Diego RossiIT
ORD-2025-0009402025-12-28 07:28:4611000.68Freya ChoiBR
ORD-2025-0015812025-12-28 06:12:382645.06Jonas LeeJP
ORD-2025-0009952025-12-28 05:47:532584.02Leo HaddadUS

브라우저의 새 탭에서 Planche가 열리고, 샘플 데이터베이스가 로드된 채로 이 쿼리가 들어갑니다. 이 페이지의 다른 예제도 같은 탭을 다시 씁니다. F9를 누르기 전까지는 아무것도 실행되지 않습니다.

JOIN만 쓰면 INNER JOIN을 뜻합니다. ON c.id = o.customer_id는 두 테이블의 행을 연결하는 조인 조건입니다. orders o처럼 붙인 별칭은 o.total, c.country처럼 컬럼을 짧게 쓰게 해 줍니다.

두 테이블에 같은 이름의 컬럼이 있으면 별칭이 필요합니다. 여기서는 두 테이블 모두 id를 갖고 있으므로 id만 쓰면 어느 테이블의 컬럼인지 알 수 없습니다. 데이터베이스도 이런 쿼리를 실행하지 않습니다.

테이블 셋 이상

조인은 여러 번 이어 쓸 수 있습니다. 주문의 상품 정보는 orders → order_items → products 순서로 두 단계를 거쳐 연결됩니다.

2

SELECT o.order_number,
       p.name AS product,
       oi.quantity,
       oi.unit_price
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'delivered'
ORDER BY o.ordered_at DESC
LIMIT 25;
Planche에서 실행

결과 — 25행 중 처음 5행

order_numberproductquantityunit_price
ORD-2025-000505Zen Boots1700.65
ORD-2025-000505Echo Backpack 40L Plus11570.96
ORD-2025-001581Atlas Phone Pro Max11546.38
ORD-2025-001581Pulse Headphones2548.09
ORD-2025-000995Aria Chrono21289.56

JOIN 한 줄은 앞선 결과에 테이블 하나를 더 연결합니다. 네 테이블을 조인하려면 같은 방식으로 한 줄을 더 추가하면 됩니다. 이 예제는 주문, 주문 항목, 각 항목의 상품 순서로 읽을 수 있습니다.

짝이 없는 행도 남기기

INNER JOIN은 짝이 없는 행을 결과에서 뺍니다. 주문한 적 없는 고객은 보이지 않습니다. LEFT JOIN은 왼쪽 테이블의 모든 행을 남기고, 연결할 값이 없는 쪽은 NULL로 채웁니다.

3

SELECT c.id,
       c.first_name,
       c.last_name,
       o.order_number,
       o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.id
LIMIT 25;
Planche에서 실행

결과 — 25행 중 처음 5행

idfirst_namelast_nameorder_numbertotal
1LiamFischerORD-2022-0015961317.56
1LiamFischerORD-2025-0017232664.56
1LiamFischerORD-2025-0031424609.77
2MateoCostaORD-2025-0024733760.6
3YukiJangORD-2025-0035277569.3

이 특성으로 연결되지 않은 행을 찾을 수 있습니다. 왼쪽 테이블의 행을 모두 남긴 뒤, 오른쪽 값이 NULL인 행만 고르면 됩니다.

4

-- The 95 customers who have never ordered anything.
SELECT c.id, c.first_name, c.last_name, c.country
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.id
LIMIT 25;
Planche에서 실행

결과 — 25행 중 처음 5행

idfirst_namelast_namecountry
14MayaMoreauUS
17NoraMüllerIT
20SanaParkKR
30RaviParkKR
33OliverParkIN

WHERE o.id IS NULL은 처음에는 낯설 수 있습니다. orders.id는 기본 키라 원래 테이블에서는 NULL일 수 없지만, LEFT JOIN은 연결되지 않은 행의 오른쪽 컬럼을 NULL로 채웁니다. 이 조건에 해당하는 95명은 주문한 적 없는 고객입니다.

ON이냐 WHERE냐: 같은 질문이 아닙니다

아래 두 쿼리에는 모두 status = 'cancelled'가 있지만, LEFT JOIN의 결과는 다릅니다.

5

-- The condition is part of the join: every customer is kept,
-- and only cancelled orders are attached to them.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
Planche에서 실행

결과 — 25행 중 처음 5행

idfirst_nameorder_numberstatus
1LiamNULLNULL
2MateoNULLNULL
3YukiORD-2025-003530cancelled
4InesNULLNULL
5LeoNULLNULL

조건이 ON에 있으니 매칭의 일부입니다. 모든 고객이 남고, 취소된 주문이 있으면 그것만 붙습니다. 취소 주문이 없는 고객도 NULL을 달고 그대로 나옵니다.

6

-- The same condition in WHERE runs after the join and throws away
-- every row where it is not true — including the NULLs the LEFT JOIN made.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
Planche에서 실행

결과 — 25행 중 처음 5행

idfirst_nameorder_numberstatus
3YukiORD-2025-003530cancelled
6NoraORD-2023-001551cancelled
6NoraORD-2022-002224cancelled
7PriyaORD-2022-003262cancelled
10ElenaORD-2025-000182cancelled

여기서는 조건이 WHERE에 있습니다. WHERE는 조인 후에 평가되므로, LEFT JOIN이 만든 NULL 행도 조건을 통과해야 합니다. NULL = 'cancelled'는 참이 아니어서 이 행들은 모두 제거됩니다. 결과적으로 INNER JOIN과 같은 결과가 됩니다.

LEFT JOIN에서는 오른쪽 테이블을 매칭하는 조건은 ON에, 왼쪽 테이블을 거르는 조건은 WHERE에 둡니다. INNER JOIN에서는 결과가 같을 수 있어 더 헷갈리기 쉽습니다.

테이블을 자기 자신과 조인하기

조인 대상이 항상 서로 다른 테이블일 필요는 없습니다. 직원 테이블의 manager_id는 다른 직원을 가리키므로, 같은 테이블을 서로 다른 별칭으로 두 번 참조합니다.

7

SELECT e.first_name || ' ' || e.last_name AS employee,
       e.position,
       m.first_name || ' ' || m.last_name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id
ORDER BY manager, employee
LIMIT 25;
Planche에서 실행

결과 — 25행 중 처음 5행

employeepositionmanager
Ava MüllerDirectorNULL
Hana TanakaDirectorNULL
Hugo AhmedDirectorNULL
Lucas RomeroDirectorNULL
Maya MüllerDirectorNULL

e와 m은 같은 테이블을 가리키지만 별칭으로 구분됩니다. 조직도 최상위 직원 열 명은 관리자가 없으므로 LEFT JOIN을 사용합니다. INNER JOIN을 쓰면 이 직원들은 결과에 포함되지 않습니다.

여기까지로 할 수 있는 것

스키마 전체에 걸친 질문을 할 수 있습니다. 아직 할 수 없는 것은 결과를 요약하는 일입니다. 주문 수를 세거나 매출을 합치거나 평균을 내는 작업은 다음 레슨에서 다룹니다.

직접 해보기

  1. 가장 최근 리뷰 15개를, 작성 시각·어떤 상품인지·누가 썼는지·별점과 함께 보여주세요. 테이블 셋입니다.

    정답 보기

    정답 보기

    SELECT r.created_at,
           p.name AS product,
           c.first_name || ' ' || c.last_name AS reviewer,
           r.rating
    FROM reviews r
    JOIN products p ON p.id = r.product_id
    JOIN customers c ON c.id = r.customer_id
    ORDER BY r.created_at DESC
    LIMIT 15;
    Planche에서 실행

    결과 — 15행 중 처음 5행

    created_atproductreviewerrating
    2025-12-28 21:12:12Sol ChronoHugo Park2
    2025-12-28 16:46:22Ember Catan 2Minjun Novak4
    2025-12-28 14:00:00Quill SpeakerOmar Smith3
    2025-12-28 03:41:42Learning React (Deluxe ed.)Ines Romero4
    2025-12-28 03:40:06Zen Sheet Set 2Kenji Ahmed4

    조인 둘을 이어 붙입니다. 리뷰에서 상품으로 가서 이름을 얻고, 리뷰에서 고객으로 가서 작성자를 얻습니다. 리뷰에는 항상 둘 다 있으므로 이너 조인입니다.

  2. 배송 기록이 아예 없는 주문은 어떤 것들인가요?

    정답 보기

    정답 보기

    SELECT o.order_number, o.status, o.ordered_at
    FROM orders o
    LEFT JOIN shipments s ON s.order_id = o.id
    WHERE s.id IS NULL
    ORDER BY o.ordered_at DESC
    LIMIT 20;
    Planche에서 실행

    결과 — 20행 중 처음 5행

    order_numberstatusordered_at
    ORD-2025-000940cancelled2025-12-28 07:28:46
    ORD-2025-003297processing2025-12-28 01:40:38
    ORD-2025-002729cancelled2025-12-27 23:10:11
    ORD-2025-002019paid2025-12-27 09:26:48
    ORD-2025-000337pending2025-12-27 05:29:40

    LEFT JOIN이 모든 주문을 남기고, WHERE s.id IS NULL이 맞는 게 없었던 것만 남깁니다. 4,000건 중 1,681건이 해당하는데, 대기 중이거나 취소된 주문은 배송되지 않으니 말이 됩니다.