여러 테이블을 연결해 답을 찾는 조인
테이블은 나뉘어 있고, 질문은 이어져 있습니다
지난 장에서 테이블을 고객, 주문, 상품처럼 나누어 설계하는 이유를 배웠습니다. 나누어 저장하면 중복이 줄고 수정이 쉬워집니다. 하지만 업무 질문은 보통 한 테이블 안에서 끝나지 않습니다.
예를 들어 orders 테이블에는 주문 정보가 들어 있습니다.
| order_id | customer_id | order_date | order_status |
|---|---|---|---|
| 1001 | 1 | 2026-06-01 | 결제완료 |
| 1002 | 2 | 2026-06-03 | 배송준비 |
| 1003 | 1 | 2026-06-05 | 결제완료 |
이 표만 보면 customer_id가 1번인 고객이 주문했다는 사실은 알 수 있습니다. 하지만 그 고객의 이름은 알 수 없습니다. 고객 이름은 customers 테이블에 따로 있기 때문입니다.
| customer_id | customer_name | city |
|---|---|---|
| 1 | 김민지 | 서울 |
| 2 | 박준호 | 부산 |
| 3 | 이서연 | 대전 |
이럴 때 필요한 것이 JOIN입니다. JOIN은 따로 있는 장부들을 연결해 하나의 결과표로 보여 주는 방법입니다. 엑셀에서 두 시트를 고객 번호로 맞춰 붙이는 작업과 비슷합니다.
AI 생성 이미지
외래키는 테이블 사이의 연결고리입니다
orders 테이블의 customer_id는 주문한 고객을 가리킵니다. 이 값은 customers 테이블의 customer_id와 연결됩니다.
customers.customer_id: 고객표에서 고객 한 명을 구분하는 번호orders.customer_id: 주문표에서 "이 주문을 한 고객이 누구인가"를 나타내는 번호
이처럼 다른 테이블의 기본키를 가리키는 컬럼을 외래키라고 합니다. 쉽게 말해, 외래키는 장부와 장부를 이어 주는 연결고리입니다.
외래키가 있으면 데이터베이스는 "존재하지 않는 고객 번호로 주문을 넣는 실수"를 막을 수 있습니다. 예를 들어 고객표에 99번 고객이 없는데 주문표에 customer_id = 99가 들어가면 나중에 주문자를 찾을 수 없습니다. 외래키 제약 조건은 이런 잘못된 연결을 줄여 줍니다.
다만 JOIN 자체는 외래키 제약 조건이 없어도 사용할 수 있습니다. 두 테이블에 서로 맞춰 볼 컬럼이 있으면 연결할 수 있습니다. 하지만 실무에서는 외래키가 명확히 잡혀 있을수록 데이터가 더 안전하고 SQL도 이해하기 쉽습니다.
INNER JOIN: 양쪽에 모두 있는 데이터만 보기
가장 기본적인 조인은 INNER JOIN입니다. INNER JOIN은 두 테이블을 연결하되, 양쪽에 서로 맞는 데이터가 있는 행만 보여 줍니다.
먼저 고객 이름이 붙은 주문 목록을 조회해 보겠습니다.
무엇을 하려는 문장인지 먼저 풀어 쓰면 이렇습니다.
주문표와 고객표를 고객 번호로 연결해서, 주문번호·고객명·주문일·주문상태를 보여 주세요.
SQL 문장은 다음과 같습니다.
SELECT
o.order_id,
c.customer_name,
o.order_date,
o.order_status
FROM orders AS o
INNER JOIN customers AS c
ON o.customer_id = c.customer_id
ORDER BY o.order_date DESC;결과는 다음처럼 나올 수 있습니다.
| order_id | customer_name | order_date | order_status |
|---|---|---|---|
| 1003 | 김민지 | 2026-06-05 | 결제완료 |
| 1002 | 박준호 | 2026-06-03 | 배송준비 |
| 1001 | 김민지 | 2026-06-01 | 결제완료 |
이 SQL을 천천히 읽어 보겠습니다.
FROM orders AS o주문표를 기준으로 조회를 시작합니다. orders AS o는 orders 테이블을 앞으로 짧게 o라고 부르겠다는 뜻입니다.
INNER JOIN customers AS c고객표를 함께 연결합니다. customers AS c는 customers 테이블을 짧게 c라고 부르겠다는 뜻입니다.
ON o.customer_id = c.customer_id두 테이블을 어떤 기준으로 맞출지 정합니다. 주문표의 고객 번호와 고객표의 고객 번호가 같으면 같은 고객의 데이터로 보고 한 줄로 붙입니다.
JOIN에서 가장 중요한 부분은 ON입니다. ON은 "두 장부의 어떤 칸을 보고 같은 기록이라고 판단할 것인가"를 정하는 규칙입니다.
테이블 별칭으로 SQL을 짧고 읽기 쉽게 만들기
조인을 쓰면 여러 테이블이 한 문장에 등장합니다. 이때 컬럼 이름만 쓰면 헷갈릴 수 있습니다.
예를 들어 customer_id는 customers에도 있고 orders에도 있습니다. 그냥 customer_id라고 쓰면 PostgreSQL 입장에서는 어느 테이블의 고객 번호인지 애매할 수 있습니다.
그래서 조인에서는 보통 다음처럼 테이블 이름 또는 별칭을 붙입니다.
o.customer_id
c.customer_id여기서 o는 orders, c는 customers를 뜻합니다. 별칭은 엑셀 파일에서 시트 이름이 길 때 짧은 이름표를 붙이는 것과 비슷합니다.
별칭을 쓰지 않으면 SQL이 길어집니다.
orders.customer_id = customers.customer_id별칭을 쓰면 짧아집니다.
o.customer_id = c.customer_id처음에는 별칭이 낯설 수 있지만, 조인 SQL에서는 별칭을 쓰는 편이 훨씬 읽기 쉽습니다. 실무에서도 거의 항상 별칭을 사용합니다.
누가 무엇을 언제 샀는지 한 번에 보기
업무에서 자주 묻는 질문은 이것입니다.
누가, 무엇을, 언제 샀나요?
이 질문에 답하려면 하나의 테이블만으로는 부족합니다.
누가:
customers언제:
orders무엇을 몇 개:
order_items상품 이름:
products
예시 테이블의 관계는 다음과 같습니다.
orders.customer_id→customers.customer_idorder_items.order_id→orders.order_idorder_items.product_id→products.product_id
order_items는 주문 안에 들어 있는 상품 목록입니다. 한 주문에 상품이 여러 개 들어갈 수 있기 때문에 주문표와 상품표 사이에 중간 장부처럼 놓입니다.
무엇을 하려는 문장인지 먼저 풀어 쓰면 이렇습니다.
주문, 고객, 주문상품, 상품 테이블을 연결해서 고객이 어떤 상품을 몇 개 샀는지 주문일과 함께 보여 주세요.
SQL 문장은 다음과 같습니다.
SELECT
o.order_id,
c.customer_name,
p.product_name,
oi.quantity,
o.order_date
FROM orders AS o
INNER JOIN customers AS c
ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi
ON o.order_id = oi.order_id
INNER JOIN products AS p
ON oi.product_id = p.product_id
ORDER BY o.order_date DESC, o.order_id;결과는 다음처럼 나올 수 있습니다.
| order_id | customer_name | product_name | quantity | order_date |
|---|---|---|---|---|
| 1003 | 김민지 | 무선 마우스 | 1 | 2026-06-05 |
| 1002 | 박준호 | USB 허브 | 2 | 2026-06-03 |
| 1001 | 김민지 | 키보드 | 1 | 2026-06-01 |
| 1001 | 김민지 | 노트북 파우치 | 1 | 2026-06-01 |
여기서 눈여겨볼 점이 있습니다. order_id 1001이 두 줄로 보입니다. 오류가 아닙니다. 1001번 주문에 상품이 두 개 들어 있기 때문에 상품 기준으로 두 줄이 나온 것입니다.
JOIN을 하면 결과 행 수가 원래 테이블보다 많아질 수 있습니다. 특히 주문과 주문상품처럼 "주문 1건에 상품 여러 개" 관계에서는 자연스러운 현상입니다. 조인 결과를 볼 때는 "한 줄이 무엇을 의미하는가"를 먼저 확인해야 합니다.
위 결과에서 한 줄은 "주문 한 건"이 아니라 "주문 안의 상품 한 줄"입니다.
LEFT JOIN: 없어도 보여 주기
INNER JOIN은 양쪽에 모두 맞는 데이터만 보여 줍니다. 그래서 주문이 없는 고객은 결과에서 빠집니다.
하지만 업무에서는 빠진 데이터를 찾아야 할 때가 많습니다.
아직 주문이 없는 고객
담당자가 지정되지 않은 거래
상품 정보가 연결되지 않은 주문상품
최근 상담 이력이 없는 고객
이럴 때 사용하는 것이 LEFT JOIN입니다.
LEFT JOIN은 왼쪽 테이블의 데이터는 모두 보여 주고, 오른쪽 테이블에 맞는 데이터가 있으면 붙입니다. 없으면 오른쪽 컬럼은 NULL로 표시됩니다.
무엇을 하려는 문장인지 먼저 풀어 쓰면 이렇습니다.
모든 고객을 보여 주되, 주문이 있으면 주문번호와 주문일도 함께 보여 주세요. 주문이 없어도 고객은 결과에 남겨 주세요.
SQL 문장은 다음과 같습니다.
SELECT
c.customer_id,
c.customer_name,
o.order_id,
o.order_date
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_date;결과는 다음처럼 나올 수 있습니다.
| customer_id | customer_name | order_id | order_date |
|---|---|---|---|
| 1 | 김민지 | 1001 | 2026-06-01 |
| 1 | 김민지 | 1003 | 2026-06-05 |
| 2 | 박준호 | 1002 | 2026-06-03 |
| 3 | 이서연 | NULL | NULL |
이서연 고객은 주문이 없습니다. 하지만 LEFT JOIN을 사용했기 때문에 결과에서 사라지지 않았습니다. 대신 주문번호와 주문일이 NULL로 나옵니다.
NULL은 값이 없다는 뜻입니다. 숫자 0이나 빈 문자열과 다릅니다. 여기서는 "연결된 주문 기록이 없다"는 의미입니다.
주문이 없는 고객만 찾기
LEFT JOIN은 누락을 찾을 때 특히 유용합니다. 위 결과에서 주문이 없는 고객만 보고 싶다면 오른쪽 테이블의 값이 NULL인 행을 고르면 됩니다.
무엇을 하려는 문장인지 먼저 풀어 쓰면 이렇습니다.
고객표를 기준으로 주문표를 붙인 뒤, 연결된 주문이 없는 고객만 보여 주세요.
SQL 문장은 다음과 같습니다.
SELECT
c.customer_id,
c.customer_name,
c.city
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL
ORDER BY c.customer_id;결과는 다음처럼 나올 수 있습니다.
| customer_id | customer_name | city |
|---|---|---|
| 3 | 이서연 | 대전 |
이 문장은 고객 관리 업무에서 바로 사용할 수 있습니다. 예를 들어 가입은 했지만 아직 첫 주문을 하지 않은 고객을 찾아 쿠폰을 보낼 수 있습니다.
같은 방식으로 담당자가 비어 있는 거래도 찾을 수 있습니다. 예를 들어 deals 테이블에 manager_id가 있고, employees 테이블에 직원 정보가 있다면 거래표를 기준으로 직원표를 LEFT JOIN한 뒤 직원 쪽 값이 NULL인 행을 찾으면 됩니다.
핵심은 같습니다.
기준 테이블은 모두 남기고, 연결이 안 된 행을
NULL로 찾아낸다.
INNER JOIN과 LEFT JOIN을 고르는 기준
두 조인의 차이는 간단합니다.
| 구분 | 결과에 남는 데이터 | 주로 쓰는 상황 |
|---|---|---|
| INNER JOIN | 양쪽에 모두 연결이 있는 데이터 | 주문한 고객 목록, 판매된 상품 목록 |
| LEFT JOIN | 왼쪽 테이블은 모두 남기고 오른쪽은 있으면 붙임 | 주문이 없는 고객 찾기, 누락된 연결 확인 |
질문이 "연결된 데이터만 보고 싶다"라면 INNER JOIN을 먼저 생각합니다.
주문이 있는 고객만 보고 싶다
실제 판매된 상품만 보고 싶다
결제 내역이 있는 주문만 보고 싶다
질문이 "없어도 기준 목록은 남겨야 한다"라면 LEFT JOIN을 생각합니다.
주문이 없는 고객도 보고 싶다
담당자가 없는 거래를 찾고 싶다
상품 정보가 연결되지 않은 주문상품을 점검하고 싶다
LEFT JOIN에서 중요한 것은 어느 테이블을 왼쪽에 둘 것인가입니다. FROM에 먼저 쓴 테이블이 왼쪽 기준 테이블입니다.
FROM customers AS c
LEFT JOIN orders AS o이 문장은 고객을 기준으로 주문을 붙입니다. 그래서 주문이 없는 고객도 남습니다.
반대로 주문을 기준으로 고객을 붙이면 결과의 의미가 달라집니다.
FROM orders AS o
LEFT JOIN customers AS c이 문장은 주문을 기준으로 고객을 붙입니다. 그래서 주문이 없는 고객은 애초에 기준 테이블에 없으므로 결과에 나오지 않습니다.
LEFT JOIN은 "왼쪽을 모두 남긴다"는 말만 기억해도 절반은 이해한 것입니다.
조인 SQL을 읽는 순서
조인 SQL이 길어지면 SELECT부터 읽기보다 FROM부터 읽는 편이 쉽습니다.
다음 순서로 읽어 보세요.
FROM: 기준이 되는 테이블은 무엇인가?JOIN: 어떤 테이블을 추가로 붙이는가?ON: 두 테이블을 어떤 컬럼으로 연결하는가?SELECT: 결과에서 어떤 컬럼을 보여 줄 것인가?WHERE: 연결한 결과 중 어떤 행만 남길 것인가?ORDER BY: 어떤 순서로 정렬할 것인가?
예를 들어 다음 문장을 보겠습니다.
SELECT
c.customer_name,
o.order_id,
o.order_date
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;이 SQL은 이렇게 읽으면 됩니다.
고객표를 기준으로 시작한다.
주문표를 고객 번호로 붙인다.
주문이 없는 고객도 남긴다.
그중 주문번호가 없는 행만 고른다.
결과로 고객명, 주문번호, 주문일을 보여 준다.
겉으로는 SQL이 길어 보이지만, 실제 구조는 단순합니다. 기준 장부를 정하고, 연결할 장부를 붙이고, 필요한 줄과 칸만 고르는 것입니다.
조인할 때 자주 하는 실수
조인에서 가장 흔한 실수는 연결 조건을 잘못 쓰는 것입니다.
ON o.order_id = c.customer_id이런 식으로 주문번호와 고객번호를 연결하면 의미가 맞지 않습니다. 숫자가 우연히 같을 수는 있지만, 같은 종류의 번호가 아닙니다. 조인은 값이 같다는 것만으로 충분하지 않습니다. 업무적으로 같은 의미의 컬럼을 연결해야 합니다.
두 번째 실수는 ON을 빠뜨리거나 너무 넓게 연결하는 것입니다. 그러면 주문과 고객이 엉뚱하게 많이 붙어 결과 행 수가 폭발적으로 늘어날 수 있습니다. 결과가 예상보다 너무 많다면 먼저 ON 조건을 확인하세요.
세 번째 실수는 LEFT JOIN 뒤에 WHERE 조건을 잘못 거는 것입니다. 예를 들어 모든 고객을 남기고 싶어서 LEFT JOIN을 썼는데, WHERE o.order_status = '결제완료'를 걸면 주문이 없는 고객은 제외될 수 있습니다. 오른쪽 테이블의 조건을 걸 때는 "NULL인 행을 남겨야 하는가"를 함께 생각해야 합니다.
처음에는 다음 체크리스트만 기억하면 충분합니다.
최종 질문은 무엇인가?
기준 테이블은 무엇인가?
어떤 컬럼끼리 연결해야 하는가?
연결된 데이터만 볼 것인가, 없어도 남길 것인가?
결과 한 줄은 무엇을 의미하는가?
조인은 흩어진 장부를 하나의 답으로 만드는 기술입니다
테이블을 잘 나누는 것만큼 중요한 일이, 나뉜 테이블을 다시 잘 연결하는 것입니다. 고객표, 주문표, 상품표가 따로 있으면 관리하기 쉽습니다. JOIN을 사용하면 그 표들을 필요한 순간에 하나의 업무용 결과표로 합칠 수 있습니다.
INNER JOIN은 양쪽에 모두 있는 데이터만 보여 줍니다. LEFT JOIN은 기준 테이블을 모두 남기고 연결된 데이터가 없으면 NULL로 보여 줍니다. 별칭을 사용하면 긴 테이블 이름을 짧게 줄여 SQL을 읽기 쉽게 만들 수 있습니다.
조인은 처음에는 어렵게 느껴지지만, 핵심은 단순합니다.
어떤 장부를 기준으로 삼고, 어떤 번호로 다른 장부를 붙일 것인가?
이 질문에 답할 수 있으면 고객, 주문, 상품, 담당자, 결제 내역처럼 흩어진 업무 데이터를 하나의 결과로 연결할 수 있습니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.