JOIN

1. JOIN이란?
JOIN은 두 개 이상의 테이블을 결합하여 관련된 데이터를 조회할 때 사용되는 SQL 구문입니다. 여러 테이블에서 필요한 데이터를 한 번에 가져오고 싶을 때, 공통된 키를 기준으로 테이블을 결합하여 다양한 결과를 얻을 수 있습니다.
JOIN의 필요성:
- 정규화된 데이터베이스에서 분산된 정보를 통합
- 관계형 데이터의 연관성을 활용한 조회
- 중복 데이터 없이 효율적인 데이터 관리
이번 장에서는 가장 많이 사용되는 INNER JOIN과 LEFT JOIN 2개를 실습하며 각 JOIN의 특징을 알아보겠습니다.
1.1 INNER JOIN
INNER JOIN은 두 테이블에서 공통된 열(키 값)이 일치하는 데이터만 반환합니다. 테이블 간 일치하는 데이터가 없으면 결과에 포함되지 않습니다.
INNER JOIN의 특징:
- 양쪽 테이블 모두에 존재하는 데이터만 반환
- 가장 일반적으로 사용되는 JOIN 유형
- NULL 값이 있는 경우 해당 행은 제외됨
활용 사례:
- 주문한 고객의 정보 조회
- 재고가 있는 상품의 카테고리 정보 조회
- 배정된 담당자가 있는 프로젝트 조회
기본 사용
SELECT 주문.주문ID, 고객.이름, 주문.주문날짜
FROM 주문
INNER JOIN 고객 ON 주문.고객ID = 고객.고객ID;
실습환경 바로가기
이 쿼리는 주문 테이블과 고객 테이블에서 공통된 고객ID를 기준으로 결합하여, 고객의 이름과 주문 날짜를 조회합니다.
실무 예제
-- 주문 상세 정보 조회 (고객명, 주문일, 주문금액)
SELECT
주문.주문ID,
고객.이름 AS 고객명,
고객.연락처,
주문.주문날짜,
주문.총액
FROM 주문
INNER JOIN 고객 ON 주문.고객ID = 고객.고객ID
ORDER BY 주문.주문날짜 DESC;
-- 3개 테이블 JOIN: 주문 + 고객 + 주문상세
SELECT
주문.주문ID,
고객.이름 AS 고객명,
상품.상품명,
주문상세.수량,
주문상세.가격,
주문상세.수량 * 주문상세.가격 AS 소계
FROM 주문
INNER JOIN 고객 ON 주문.고객ID = 고객.고객ID
INNER JOIN 주문상세 ON 주문.주문ID = 주문상세.주문ID
INNER JOIN 상품 ON 주문상세.상품ID = 상품.상품ID
WHERE 주문.주문날짜 >= DATE('now', '-30 days');
-- 카테고리별 판매 현황
SELECT
카테고리.카테고리명,
COUNT(DISTINCT 주문상세.주문ID) AS 주문건수,
SUM(주문상세.수량) AS 총판매수량,
SUM(주문상세.수량 * 주문상세.가격) AS 총매출
FROM 주문상세
INNER JOIN 상품 ON 주문상세.상품ID = 상품.상품ID
INNER JOIN 카테고리 ON 상품.카테고리ID = 카테고리.카테고리ID
GROUP BY 카테고리.카테고리명
ORDER BY 총매출 DESC;
1.2 LEFT JOIN
LEFT JOIN은 왼쪽(기준) 테이블의 모든 행을 반환하고, 오른쪽(조인된) 테이블에서 일치하는 데이터가 없으면 NULL 값을 반환합니다. 일치하는 데이터가 없는 경우에도 왼쪽 테이블의 모든 데이터가 반환됩니다.
LEFT JOIN의 특징:
- 왼쪽(기준) 테이블의 모든 데이터 보존
- 오른쪽 테이블에 매칭되는 데이터가 없으면 NULL 반환
- 누락된 관계를 찾을 때 유용
활용 사례:
- 주문하지 않은 고객 찾기
- 할당되지 않은 리소스 확인
- 전체 목록과 실적 비교
기본 사용
SELECT 고객.이름, 주문.주문ID
FROM 고객
LEFT JOIN 주문 ON 고객.고객ID = 주문.고객ID;
실습환경 바로가기
이 쿼리는 고객 테이블의 모든 고객을 반환하며, 해당 고객이 주문한 내역이 있으면 함께 조회되고, 주문 내역이 없는 고객의 경우 NULL로 표시됩니다.
실무 예제
-- 주문하지 않은 고객 찾기
SELECT
고객.고객ID,
고객.이름,
고객.이메일
FROM 고객
LEFT JOIN 주문 ON 고객.고객ID = 주문.고객ID
WHERE 주문.주문ID IS NULL;
-- 고객별 주문 통계 (주문이 없는 고객도 포함)
SELECT
고객.이름,
COUNT(주문.주문ID) AS 주문수,
COALESCE(SUM(주문.총액), 0) AS 총구매금액,
CASE
WHEN COUNT(주문.주문ID) = 0 THEN '미구매고객'
WHEN COUNT(주문.주문ID) <= 2 THEN '일반고객'
ELSE '우수고객'
END AS 고객등급
FROM 고객
LEFT JOIN 주문 ON 고객.고객ID = 주문.고객ID
GROUP BY 고객.고객ID, 고객.이름
ORDER BY 총구매금액 DESC;
-- 최근 3개월 내 주문이 없는 고객 (이탈 위험 고객)
SELECT
고객.고객ID,
고객.이름,
고객.연락처,
MAX(주문.주문날짜) AS 마지막주문일,
ROUND(JULIANDAY('now') - JULIANDAY(MAX(주문.주문날짜))) AS 미주문일수
FROM 고객
LEFT JOIN 주문 ON 고객.고객ID = 주문.고객ID
GROUP BY 고객.고객ID, 고객.이름, 고객.연락처
HAVING MAX(주문.주문날짜) < DATE('now', '-90 days')
OR MAX(주문.주문날짜) IS NULL
ORDER BY 마지막주문일;
-- 카테고리별 상품 및 판매 현황 (판매 실적이 없는 카테고리도 포함)
SELECT
카테고리.카테고리명,
COUNT(DISTINCT 상품.상품ID) AS 등록상품수,
COUNT(DISTINCT 주문상세.주문ID) AS 판매건수,
COALESCE(SUM(주문상세.수량), 0) AS 총판매수량,
COALESCE(SUM(주문상세.수량 * 주문상세.가격), 0) AS 총매출
FROM 카테고리
LEFT JOIN 상품 ON 카테고리.카테고리ID = 상품.카테고리ID
LEFT JOIN 주문상세 ON 상품.상품ID = 주문상세.상품ID
GROUP BY 카테고리.카테고리ID, 카테고리.카테고리명
ORDER BY 총매출 DESC;
2. INNER JOIN vs LEFT JOIN 비교
선택 기준:
-
INNER JOIN: 양쪽 테이블에 모두 존재하는 데이터만 필요할 때
- 예: 실제 주문한 고객의 정보만 조회
- 예: 배정된 담당자가 있는 프로젝트만 조회
-
LEFT JOIN: 기준 테이블의 모든 데이터가 필요하고, 관련 데이터가 있으면 함께 조회할 때
- 예: 전체 고객 목록과 주문 여부 확인
- 예: 모든 상품과 판매 실적 조회
실전 예제: 같은 데이터로 두 JOIN 비교
-- INNER JOIN: 주문한 고객만 (10명)
SELECT
고객.이름,
COUNT(주문.주문ID) AS 주문수
FROM 고객
INNER JOIN 주문 ON 고객.고객ID = 주문.고객ID
GROUP BY 고객.고객ID, 고객.이름;
-- LEFT JOIN: 모든 고객 (주문 안한 고객도 포함, 15명)
SELECT
고객.이름,
COUNT(주문.주문ID) AS 주문수
FROM 고객
LEFT JOIN 주문 ON 고객.고객ID = 주문.고객ID
GROUP BY 고객.고객ID, 고객.이름;
마무리
JOIN은 관계형 데이터베이스의 핵심 기능으로, 여러 테이블에 분산된 데이터를 효과적으로 결합하여 조회할 수 있게 해줍니다.
JOIN 활용 팁:
- 적절한 인덱스를 사용하면 JOIN 성능이 크게 향상됩니다
- 테이블 별칭(alias)을 사용하면 쿼리가 간결해집니다
- WHERE 조건은 JOIN 조건과 명확히 구분하세요
- LEFT JOIN에서 NULL 체크를 활용하여 누락된 관계를 찾을 수 있습니다
다음 장에서는 더 복잡한 데이터 조작과 서브쿼리에 대해 알아보겠습니다.