기본 조인
주문에는 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;
결과 — 20행 중 처음 5행
| order_number | ordered_at | total | customer | country |
|---|---|---|---|---|
| ORD-2025-002480 | 2025-12-28 23:52:14 | 6333.77 | Elena Meyer | CA |
| ORD-2025-000505 | 2025-12-28 11:16:39 | 2276.51 | Diego Rossi | IT |
| ORD-2025-000940 | 2025-12-28 07:28:46 | 11000.68 | Freya Choi | BR |
| ORD-2025-001581 | 2025-12-28 06:12:38 | 2645.06 | Jonas Lee | JP |
| ORD-2025-000995 | 2025-12-28 05:47:53 | 2584.02 | Leo Haddad | US |
브라우저의 새 탭에서 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;
결과 — 25행 중 처음 5행
| order_number | product | quantity | unit_price |
|---|---|---|---|
| ORD-2025-000505 | Zen Boots | 1 | 700.65 |
| ORD-2025-000505 | Echo Backpack 40L Plus | 1 | 1570.96 |
| ORD-2025-001581 | Atlas Phone Pro Max | 1 | 1546.38 |
| ORD-2025-001581 | Pulse Headphones | 2 | 548.09 |
| ORD-2025-000995 | Aria Chrono | 2 | 1289.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;
결과 — 25행 중 처음 5행
| id | first_name | last_name | order_number | total |
|---|---|---|---|---|
| 1 | Liam | Fischer | ORD-2022-001596 | 1317.56 |
| 1 | Liam | Fischer | ORD-2025-001723 | 2664.56 |
| 1 | Liam | Fischer | ORD-2025-003142 | 4609.77 |
| 2 | Mateo | Costa | ORD-2025-002473 | 3760.6 |
| 3 | Yuki | Jang | ORD-2025-003527 | 7569.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;
결과 — 25행 중 처음 5행
| id | first_name | last_name | country |
|---|---|---|---|
| 14 | Maya | Moreau | US |
| 17 | Nora | Müller | IT |
| 20 | Sana | Park | KR |
| 30 | Ravi | Park | KR |
| 33 | Oliver | Park | IN |
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;
결과 — 25행 중 처음 5행
| id | first_name | order_number | status |
|---|---|---|---|
| 1 | Liam | NULL | NULL |
| 2 | Mateo | NULL | NULL |
| 3 | Yuki | ORD-2025-003530 | cancelled |
| 4 | Ines | NULL | NULL |
| 5 | Leo | NULL | NULL |
조건이 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;
결과 — 25행 중 처음 5행
| id | first_name | order_number | status |
|---|---|---|---|
| 3 | Yuki | ORD-2025-003530 | cancelled |
| 6 | Nora | ORD-2023-001551 | cancelled |
| 6 | Nora | ORD-2022-002224 | cancelled |
| 7 | Priya | ORD-2022-003262 | cancelled |
| 10 | Elena | ORD-2025-000182 | cancelled |
여기서는 조건이 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;
결과 — 25행 중 처음 5행
| employee | position | manager |
|---|---|---|
| Ava Müller | Director | NULL |
| Hana Tanaka | Director | NULL |
| Hugo Ahmed | Director | NULL |
| Lucas Romero | Director | NULL |
| Maya Müller | Director | NULL |
e와 m은 같은 테이블을 가리키지만 별칭으로 구분됩니다. 조직도 최상위 직원 열 명은 관리자가 없으므로 LEFT JOIN을 사용합니다. INNER JOIN을 쓰면 이 직원들은 결과에 포함되지 않습니다.
여기까지로 할 수 있는 것
스키마 전체에 걸친 질문을 할 수 있습니다. 아직 할 수 없는 것은 결과를 요약하는 일입니다. 주문 수를 세거나 매출을 합치거나 평균을 내는 작업은 다음 레슨에서 다룹니다.