1. 개요

관계형 데이터베이스에서는 회원, 주문처럼 성격이 다른 데이터를 보통 별도의 테이블에 나누어 저장한다. 조인(JOIN) 은 공통된 값을 기준으로 여러 테이블의 행을 한 번의 조회 결과로 연결하는 SQL 기능이다.

이 글에서는 회원과 주문 테이블을 예로 들어 조인이 필요한 이유와 INNER JOIN, LEFT JOIN의 차이를 살펴본다. 조인 전에 테이블이 어떤 관계로 연결되어 있는지 먼저 확인하는 습관이 중요하다.


2. 왜 테이블을 나누고 다시 연결할까?

회원 이름을 주문 행마다 함께 저장하면, 회원 이름이 바뀔 때 모든 주문 행을 수정해야 한다. 같은 정보를 여러 곳에 반복해 저장하면 수정이 빠지거나 값이 어긋날 가능성도 커진다.

그래서 회원 정보와 주문 정보를 각각의 테이블에 보관하고, 주문의 member_id가 회원의 member_id를 참조하게 만든다. members.member_id는 기본키이고 orders.member_id는 이를 가리키는 외래키다.

members orders
member_id ← member_id
name order_id, amount

다음 데이터가 있다고 가정한다. 회원 2는 아직 주문하지 않았고, 회원 1은 주문을 두 번 했다.

members.member_id members.name
1 민수
2 지윤
3 서준
orders.order_id orders.member_id orders.amount
101 1 12000
102 1 8000
103 3 15000

주문 목록에 회원 이름을 함께 표시하려면 두 테이블의 member_id가 같은 행을 연결해야 한다. 이때 사용하는 것이 조인이다.


3. INNER JOIN: 양쪽에 모두 있는 행만 조회하기

INNER JOIN은 조인 조건에 맞는 행이 두 테이블에 모두 있을 때만 결과에 포함한다. 주문이 있는 회원의 주문 목록을 조회할 때 알맞다.

다음 SQL은 주문 테이블과 회원 테이블의 member_id가 같은 행을 연결한다. m, o는 긴 테이블 이름을 짧게 쓰기 위한 별칭(alias)이다.

SELECT
    o.order_id,
    m.name AS member_name,
    o.amount
FROM orders AS o
INNER JOIN members AS m
    ON o.member_id = m.member_id;

조회 결과는 다음과 같다.

order_id member_name amount
101 민수 12000
102 민수 8000
103 서준 15000

주문이 없는 지윤은 orders에 연결할 행이 없으므로 나타나지 않는다. 반대로 외래키 제약 조건이 없다면 존재하지 않는 회원을 가리키는 주문도 생길 수 있는데, 이 주문 역시 연결할 회원 행이 없어서 결과에 포함되지 않는다.

JOIN 앞의 INNER는 생략할 수 있다. 아래 SQL은 앞의 SQL과 같은 의미다.

SELECT o.order_id, m.name AS member_name, o.amount
FROM orders AS o
JOIN members AS m
    ON o.member_id = m.member_id;

4. LEFT JOIN: 왼쪽 테이블의 행을 모두 남기기

모든 회원과 각 회원의 주문 정보를 함께 보고 싶을 때는 LEFT JOIN을 사용한다. 왼쪽에 쓴 테이블의 행은 조인 조건에 맞는 행이 없어도 결과에 남고, 오른쪽 테이블의 열에는 NULL이 들어간다.

아래 SQL은 members를 왼쪽에 두므로 주문하지 않은 지윤도 결과에서 확인할 수 있다.

SELECT
    m.member_id,
    m.name,
    o.order_id,
    o.amount
FROM members AS m
LEFT JOIN orders AS o
    ON m.member_id = o.member_id;
member_id name order_id amount
1 민수 101 12000
1 민수 102 8000
2 지윤 NULL NULL
3 서준 103 15000

한 회원이 주문을 여러 번 하면 회원 행도 주문 수만큼 반복된다. 이는 잘못된 중복이 아니라, 회원 한 명과 여러 주문의 관계를 행 단위로 표현한 결과다.


5. 조인 종류별 결과 비교

두 테이블을 연결하는 대표적인 조인 종류는 다음과 같다. 실무에서 가장 먼저 익힐 것은 INNER JOIN과 LEFT JOIN이다.

종류 결과에 남는 행 활용 예
INNER JOIN 양쪽에 조인 조건과 맞는 행이 있는 경우 주문이 있는 회원의 주문 내역
LEFT JOIN 왼쪽 테이블의 모든 행과 맞는 오른쪽 행 주문 여부를 포함한 전체 회원 목록
RIGHT JOIN 오른쪽 테이블의 모든 행과 맞는 왼쪽 행 테이블 순서를 바꾼 LEFT JOIN으로 대체 가능한 경우가 많음
FULL OUTER JOIN 양쪽 테이블의 모든 행 어느 쪽에만 있는 데이터도 함께 점검할 때
CROSS JOIN 양쪽 테이블의 가능한 모든 조합 모든 회원과 모든 쿠폰의 조합 만들기
SELF JOIN 같은 테이블을 서로 다른 별칭으로 연결 직원과 관리자를 한 테이블에서 함께 조회하기

RIGHT JOIN과 FULL OUTER JOIN

RIGHT JOIN은 오른쪽 테이블의 모든 행을 남긴다는 점만 LEFT JOIN과 반대다. 예를 들어 orders를 오른쪽에 두면 주문은 모두 남기고, 연결되는 회원 정보가 없을 때 회원 열을 NULL로 표시한다. 같은 결과를 테이블 순서를 바꾼 LEFT JOIN으로 쓸 수 있어, 읽는 방향을 일정하게 유지하려고 LEFT JOIN을 선호하는 경우가 많다.

FULL OUTER JOIN은 조인 조건에 맞지 않는 왼쪽과 오른쪽 행도 모두 남긴다. 두 시스템의 회원 목록처럼 한쪽에만 있는 데이터를 비교할 때 유용하다. 지원 여부와 문법은 데이터베이스 제품마다 다르므로 사용할 제품의 문서를 확인해야 한다.

CROSS JOIN: 모든 조합 만들기

CROSS JOIN은 연결 열을 비교하지 않고 두 테이블의 모든 행을 조합한다. 회원이 3명이고 쿠폰이 2개라면 결과는 3 × 2, 즉 6행이다.

SELECT m.name, c.coupon_name
FROM members AS m
CROSS JOIN coupons AS c;

이 조인은 가능한 조합을 미리 만들 때 쓸 수 있지만 행 수가 빠르게 늘어난다. 필요한 범위를 WHERE로 제한할 수 있는지 먼저 확인해야 한다.

SELF JOIN: 같은 테이블을 두 역할로 연결하기

SELF JOIN은 하나의 테이블을 서로 다른 별칭으로 두 번 사용해 자기 자신과 연결하는 방식이다. 예를 들어 직원 테이블에 각 직원의 manager_id가 같은 테이블의 employee_id를 가리킨다면, 직원과 관리자의 이름을 함께 조회할 수 있다.

SELECT
    e.name AS employee_name,
    m.name AS manager_name
FROM employees AS e
LEFT JOIN employees AS m
    ON e.manager_id = m.employee_id;

여기서 e는 직원 역할, m은 관리자 역할을 나타낸다. 둘 다 employees 테이블이지만 별칭으로 역할을 구분했기 때문에 같은 테이블의 두 행을 비교할 수 있다.


6. ON과 WHERE를 구분하기

ON은 두 테이블의 행을 어떤 조건으로 연결할지 정한다. WHERE는 조인으로 만든 결과에서 어떤 행을 남길지 거르는 조건이다.

예를 들어 주문하지 않은 회원만 찾으려면 먼저 모든 회원을 LEFT JOIN으로 남긴 다음, 주문 번호가 없는 행을 WHERE로 고른다.

SELECT
    m.member_id,
    m.name
FROM members AS m
LEFT JOIN orders AS o
    ON m.member_id = o.member_id
WHERE o.order_id IS NULL;

여기서 o.order_id = NULL이라고 쓰면 원하는 결과를 얻지 못한다. SQL에서 NULL은 값이 없거나 알 수 없음을 뜻하므로, NULL인지 검사할 때는 IS NULL 또는 IS NOT NULL을 사용한다.

또한 LEFT JOIN 뒤에 오른쪽 테이블의 조건을 WHERE에 바로 쓰면 NULL인 행이 제거되어 결과가 INNER JOIN처럼 달라질 수 있다. 왼쪽 행을 남긴 채 조건에 맞는 주문만 연결하려면 조건을 ON에 둔다.

SELECT
    m.name,
    o.order_id,
    o.amount
FROM members AS m
LEFT JOIN orders AS o
    ON m.member_id = o.member_id
    AND o.amount >= 10000;

이 쿼리는 모든 회원을 남기면서 금액이 10,000 이상인 주문만 연결한다.


7. 조인할 때 확인할 점

조인 결과가 예상보다 많아지거나 적어졌다면 다음을 먼저 점검한다.

확인할 점 이유
조인 열이 실제 관계를 표현하는가 이름처럼 중복될 수 있는 값으로 연결하면 엉뚱한 행이 묶일 수 있다.
ON 조건이 빠지지 않았는가 조건 없이 여러 행을 조합하면 결과 수가 크게 늘어난다.
한쪽 관계가 여러 개인가 회원 한 명의 여러 주문처럼, 한 행이 여러 결과 행으로 늘어나는 것은 정상일 수 있다.
필요한 조인 종류를 골랐는가 주문 없는 회원도 필요하면 INNER JOIN이 아니라 LEFT JOIN이 필요하다.

조인에 자주 쓰는 외래키 열에는 인덱스가 도움이 될 수 있다. 다만 인덱스는 쓰기 비용과 저장 공간도 늘리므로, 실제 조회 패턴과 실행 계획을 확인한 뒤 적용하는 것이 좋다.


8. 정리

조인은 분리된 테이블의 데이터를 공통 열을 기준으로 연결해 조회하는 기능이다. INNER JOIN은 양쪽에 모두 대응되는 행만, LEFT JOIN은 왼쪽 테이블의 모든 행을 보여 준다는 차이를 기억하면 기본적인 조회를 작성할 수 있다.

조인을 작성할 때는 먼저 테이블 사이의 기본키·외래키 관계를 확인하고, 결과에 꼭 남아야 하는 테이블을 정한다. 그다음 ON에는 연결 조건을, WHERE에는 최종 결과를 거르는 조건을 두면 쿼리의 의도를 더 명확하게 표현할 수 있다.