우리 쇼핑몰의 대표님이 다음과 같은 요청을 한다.
"주문이 완료된(`COMPLETED` ) 모든 주문에 대해, 어떤 고객이 주문했는지 고객 ID, 고객 이름과 주문 날짜를 함께 보고 싶네."
이 요구사항을 충족하려면 "고객 이름"을 담고 있는 "users" 테이블과, "주문 날짜"와 "주문 상태"를 담고 있는 "orders" 테이블의 정보가 모두 필요하다. 즉, 두 테이블을 연결해야만 한다. 조인은 크게 내부 조인("INNER JOIN")과 외부 조인("OUTER JOIN")으로 나눌 수 있다.
내부 조인이란
내부 조인("INNER JOIN")은 두 테이블을 연결할 때, 양쪽 테이블에 모두 공통으로 존재하는 데이터만을 결과로 보여준다. 기준이 되는 컬럼(예: "orders.user_id" 와 "users.user_id" )의 값이 서로 일치하는 행들만 짝을 지어주는 것이다
"orders" 테이블에 "user_id" 가 존재하고, 연결되는 "users" 테이블에도 해당 "user_id" 를 가진 사용자가 존재할 때만 결과에 포함된다.
- 예를 들어 "orders" 테이블에 "user_id" 가 1인 주문이 있고 "users" 테이블에 "user_id" 가 1인 회원이 있다면 둘은 함께 결과에 포함된다.
- 예를 들어 "orders" 테이블에 "user_id" 가 99인 주문이 있지만 "users" 테이블에 "user_id" 가 99인 회원이 없다면, 결과에서 제외된다. (실제로는 데이터 무결성을 위해 외래 키(FOREIGN KEY) 제약조건을 사용하므로 이런 경우는 발생하지 않는다.)
내부 조인 문법
SELECT 컬럼1, 컬럼2, ...
FROM 테이블A
INNER JOIN 테이블B
ON 테이블A.연결컬럼 = 테이블B.연결컬럼;
- FROM : 기준이 되는 첫 번째 테이블을 지정한다.
- ON : 조인에서 가장 중요한 부분이다. 두 테이블을 어떤 조건으로 연결할지 명시하는 연결고리다. ON 절의 조건이 참(true)이 되는 행들만 결과에 포함된다.
- INNER JOIN : 연결할 두 번째 테이블을 지정한다.
INNER 생략
내부 조인에서 "INNER" 는 생략할 수 있다. 그냥 "JOIN" 이라고만 쓰면 "INNER JOIN"으로 작동한다. 내부 조인을 사용하는 경우 실무에서는 대부분 "INNER" 를 생략하고 "JOIN" 만 사용한다.
실습1. 주문별 고객 정보 조회하기
select *
from orders
inner join users
on orders.user_id = users.user_id;

- 실행 결과는 "orders" 테이블의 모든 컬럼과 "users" 테이블의 "user_id" 가 같은 모든 컬럼이 옆으로 합쳐진, 아주 넓은 테이블이 된다.
- 데이터베이스는 "orders" 테이블과 "users" 테이블을 조회한다.
- JOIN ON 에 의해 "orders" 의 "user_id" 와 "users" 의 "user_id"를 기준으로 조인한다.
- "orders" 의 "user_id" 의 값으로 "users" 의 "user_id" 항목을 찾고 연결한다.
- "order_id"가 1의 경우 "user_id:1" 이다. 따라서 "users" 의 "user_id:1" 항목과 연결한다.
- "order_id:2"가 2의 경우 "user_id:1" 이다. 따라서 "users" 의 "user_id:1" 항목과 연결한다.

- 결과에서 볼 수 있듯이, "orders" 와 "users" 의 모든 컬럼이 옆으로 합쳐진다.
- "user_id" 컬럼이 두 번 나타나는 이유는 "orders" , "users" 두 테이블 모두에 존재하기 때문이다.
- SELECT에 "*" 대신 컬럼을 직접 나열하는 경우 "user_id" 처럼 중복으로 나타나는 컬럼은 테이블 명을 지정해주어야 한다.
실습2. 필요한 컬럼만 선택하고, "WHERE"로 필터링하기
select
users.user_id, -- 중복되는 데이터는 명확히 지정해줘야한다.
users.name,
orders.order_date
from orders
inner join users
on orders.user_id = users.user_id
where orders.status = 'COMPLETED'
- 여기서 "users.user_id" 는 "테이블명.컬럼명" 형식으로 작성한 것을 볼 수 있다.
- "user_id" 처럼 "orders" , "users" 두 테이블에 이름이 같은 컬럼이 있을 경우, 어떤 테이블의 "user_id" 를 말하는지 명확히 지정해주어야 한다. 그렇지 않으면 오류가 발생한다.
- 오류 메시지: Column 'user_id' in field list is ambiguous (user_id 필드가 모호하다는 오류 메시지)
- "name" , "order_date" 의 경우 한 테이블에만 존재하기 때문에 모호함이 발생하지는 않는다. 이 경우 앞의 테이블명을 생략할 수 있다.
- 실무에서는 어떤 테이블의 필드인지 명시하는 것이 가독성을 높이고, 컬럼의 소속을 쉽게 인지할 수 있기 때문에 사용하는 것을 권장한다.
논리적인 동작 순서



- FROM / JOIN: 가장 먼저 "FROM" 절의 "orders" 테이블과 "INNER JOIN"으로 연결된 "users" 테이블을 연결하기 위해 "ON"절에 명시된 "orders.user_id = users.user_id" 조건을 만족하는 행들을 결합하여 하나의 큰 가상 테이블을 생성한다.
- WHERE : "JOIN" 을 통해 생성된 가상 테이블에서 "WHERE" 절의 조건인 "orders.status = 'COMPLETED'" 를 만족하는 행들만 필터링한다. 즉, "COMPLETED" 상태의 주문 데이터만 남게 된다
- SELECT: 마지막으로, 필터링된 결과에서 "SELECT" 절에 명시된 "users.user_id" , "name" 과 "order_date" 컬럼을 추출하여 최종 결과를 반환한다.
쿼리 최적화기 (쿼리 옵티마이저)
사용자와 데이터베이스 간의 논리적 순서는 일종의 약속이지만, 데이터베이스 내부의 쿼리 최적화기(Query Optimizer)는 쿼리를 더 효율적인 방식으로 실행한다.
예를 들어, "orders" 테이블에서 "COMPLETED" 상태의 행만 먼저 선택한 다음, 남은 행을 기준으로 "users" 테이블과 조인하는 방식으로 진행하면 조인 대상이 줄어들어 성능이 더 최적화 될 수 있다.
쿼리 최적화기를 통해 실제 물리적인 실행 순서는 달라질 수 있지만 어떻게 작동하든 최종 결과는 논리적인 순서와 동일하다. 하지만 데이터베이스의 최적화 방식을 잘 이해하면 같은 결과를 얻으면서도 조회 성능을 최적화할 수 있다.
'데이터 베이스 > 데이터베이스 기본' 카테고리의 다른 글
| Ch04. UNION - UNION (0) | 2026.05.31 |
|---|---|
| Ch03. 서브쿼리 - 서브쿼리 VS Join (0) | 2026.05.30 |
| Ch01. 내부 조인 - 내부 조인(3) (0) | 2026.05.24 |
| Ch01. 내부 조인 - 내부 조인(2) (0) | 2026.05.24 |
| Ch01. 내부 조인 - 조인이 필요한 이유 (0) | 2026.05.23 |