SQL JOIN 완전 정복: 테이블을 연결하는 진짜 기술
1. JOIN이란 무엇인가?
SQL에서 JOIN은 두 개 이상의 테이블을 하나로 합쳐서 새로운 결과를 생성하는 기술이다. 데이터베이스 설계 시 같은 정보를 여러 테이블로 나누는 게 정규화의 기본인데, 그렇게 나눠진 정보를 다시 논리적으로 연결해서 하나의 통합된 뷰를 보여주는 게 JOIN이다.
간단한 예를 보자.
- users 테이블: 회원 기본 정보 보유
- orders 테이블: 주문 정보 보유
orders에 user_id만 있고, 이름이나 이메일은 없기 때문에 두 테이블을 연결해야 "누가 어떤 상품을 주문했는지" 확인할 수 있다.
2. JOIN의 종류 총정리
종류 | 설명 |
INNER JOIN | 공통되는 값이 있을 때만 결과에 포함 |
LEFT JOIN | 왼쪽 테이블의 모든 데이터를 유지하며, 오른쪽에 맞는 값이 없으면 NULL |
RIGHT JOIN | 오른쪽 테이블 기준으로 유지, 왼쪽이 없으면 NULL |
FULL JOIN | 양쪽 모두에서 데이터를 가져옴, 어느 쪽이 NULL이어도 포함 (MySQL 미지원) |
SELF JOIN | 같은 테이블을 두 번 JOIN해서 내부 관계 표현 |
CROSS JOIN | 가능한 모든 조합을 생성 (카티션 곱) |
이번 글에서는 이 중에서 실무에서 정말 많이 쓰이는 INNER JOIN, LEFT JOIN, RIGHT JOIN, SELF JOIN을 중심으로 설명한다.
3. INNER JOIN: 가장 기본이 되는 JOIN
3-1. 기본 문법
SELECT A.*, B.* FROM users A INNER JOIN orders B ON A.id = B.user_id;
→ INNER JOIN은 두 테이블 모두에 일치하는 값이 있을 때만 결과에 포함시킨다. 예를 들어, 주문한 사용자만 조회하고 싶을 때 쓰기 좋다.
3-2. 실무 예제: 주문 정보에 사용자 이름 붙이기
SELECT o.id AS order_id, u.name, o.total_price FROM orders o INNER JOIN users u ON o.user_id = u.id;
→ 주문만 존재하는 행 + 사용자 정보까지 매핑된다. 주문 없는 유저는 제외됨.
4. LEFT JOIN: 왼쪽 테이블 기준으로 전체 보기
LEFT JOIN은 왼쪽 테이블의 모든 데이터를 유지하면서, 오른쪽 테이블에 맞는 데이터가 있으면 붙이고, 없으면 NULL로 채운다.
4-1. 기본 문법
SELECT A.*, B.* FROM users A LEFT JOIN orders B ON A.id = B.user_id;
4-2. 실무 예제: 모든 유저 + 주문 내역 여부 확인
SELECT u.name, o.id AS order_id, o.total_price FROM users u LEFT JOIN orders o ON u.id = o.user_id;
→ 주문이 없는 유저도 포함되며, order_id, total_price는 NULL로 표시됨.
이 방식은 가입만 했지만 아직 주문하지 않은 고객까지 모두 보여주고 싶을 때 유용하다.
5. SELF JOIN: 같은 테이블끼리도 연결할 수 있다
SELF JOIN은 같은 테이블을 자기 자신과 조인하는 방식이다. 처음 들으면 낯설게 느껴질 수 있지만, 생각보다 자주 쓰인다. 특히, 다음과 같은 상황에서 매우 유용하다:
- 조직도: 한 직원이 다른 직원의 상사일 때
- 트리 구조: 카테고리와 하위 카테고리를 관리할 때
- 사용자 관계: 사용자가 다른 사용자를 참조하는 구조 (예: 추천인, 멘토)
5-1. 예시: 직원과 상사의 관계 구하기
SELECT e.name AS employee, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id;
5-2. 예시: 상품 카테고리 구조 표현
SELECT c.name AS category, p.name AS parent_category FROM categories c LEFT JOIN categories p ON c.parent_id = p.id;
SELF JOIN은 복잡한 구조를 평평하게 펼쳐 보여주고 싶을 때 아주 강력하다. 단, 테이블을 두 번 불러오는 만큼 **별칭(Alias)**을 꼭 써야 헷갈리지 않는다.
6. ON과 WHERE의 차이를 이해해야 한다
JOIN을 쓸 때 ON과 WHERE의 차이를 헷갈려 하는 경우가 많다. 둘 다 조건을 거는 문법이지만, 적용 시점과 목적이 다르다.
6-1. ON: 테이블 간 연결 조건 (조인 기준)
- JOIN이 실행될 때 어떻게 두 테이블을 연결할지를 정의
- ON A.id = B.a_id처럼 외래키와 기본키 등 관계 중심으로 사용
6-2. WHERE: 결과 필터링 조건
- JOIN 결과에서 원하는 값만 남기기 위한 조건
- 예: 나이 30세 이상, 주문금액 1만 원 이상 등
6-3. 예제: 회원 중에서 30세 이상 주문만 보기
SELECT u.name, o.total_price FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE u.age >= 30;
→ ON은 조인 기준(회원과 주문 연결), WHERE은 나이 조건 필터링이다.
6-4. LEFT JOIN에서의 함정
-- 잘못된 예
SELECT u.name, o.total_price FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.total_price > 10000;
정리하자면,
- ON은 연결 기준,
- WHERE은 필터 조건
- LEFT JOIN이나 OUTER JOIN에서는 WHERE 사용에 특히 주의해야 한다
7. 실무에서 JOIN을 꼭 써야 하는 상황들
JOIN은 단순히 테이블을 합치는 기술이 아니라, 정규화된 데이터 구조를 다시 하나의 화면 또는 로직으로 표현하는 핵심 수단이다.
즉, 실무에서는 JOIN 없이 시스템을 만들 수 없다고 해도 과언이 아니다.
아래는 다양한 실무 시나리오 속 JOIN의 실질적인 역할이다:
7-1. 사용자 + 주문 내역
SELECT u.name, o.total_price FROM users u JOIN orders o ON u.id = o.user_id;
→ “누가 어떤 주문을 했는가?”를 보기 위해 필수
7-2. 상품 + 카테고리명
SELECT p.name, c.name AS category_name FROM products p JOIN categories c ON p.category_id = c.id;
→ 상품만 봐서는 의미가 없고, 카테고리명이 함께 있어야 이해할 수 있다
7-3. 리뷰 + 사용자 + 상품
SELECT r.content, u.name AS reviewer, p.name AS product FROM reviews r JOIN users u ON r.user_id = u.id JOIN products p ON r.product_id = p.id;
7-4. 관리자 화면: 고객 정보 + 활동 정보 + 최근 결제 내역
이런 화면은 3~5개의 테이블을 JOIN해야 자연스럽게 데이터가 연결된다. JOIN이 없으면 모든 정보를 하나의 거대한 테이블에 집어넣어야 하고, 이는 정규화의 기본 원칙을 정면으로 위배하는 일이다. JOIN은 단순한 문법이 아니라, 시스템 아키텍처의 핵심 연결고리이다. 어떤 화면을 만들든, 어떤 API를 만들든, 데이터가 여러 테이블에 흩어져 있다면 JOIN은 반드시 필요하다.
8. 실무에서 JOIN을 꼭 써야 하는 대표 상황
- 주문 내역에 고객 정보 붙이기
- 게시글에 작성자 정보 불러오기
- 리뷰 테이블에 상품명 추가
- 팔로우 관계에서 팔로잉/팔로워 이름 불러오기
- 카테고리 ID로 이름 매핑
- 트랜잭션 이력에서 관리자 이름 붙이기
JOIN 없이 이런 작업을 한다면, 모든 데이터를 하나의 테이블에 덕지덕지 저장해야 하며, 유지보수는 사실상 불가능해진다. JOIN은 정규화된 테이블 간의 논리적 연결고리를 만들어주는 가장 핵심적인 SQL 기술이다.
9. 주의사항과 성능 고려
JOIN은 강력하지만, 조심하지 않으면 성능에 영향을 줄 수 있다.
9-1. 주의할 점
- 조인 조건을 명확히 지정하지 않으면 카티션 곱(Cartesian Product) 발생 → 수천만 건 이상 결과 나올 수 있음
- ON 없이 WHERE로만 조인하면 실수 가능성 높음
- 조인 대상 컬럼에 인덱스 부여 필요 (특히 외래키 기준 컬럼)
- LEFT JOIN + WHERE table2.column = ... → NULL이 필터링되어 사실상 INNER JOIN처럼 작동하므로 주의