[SQL] JOIN
정의
JOIN 은 두 개 이상의 테이블을 공통 컬럼 (보통 키) 으로 결합해 한 결과셋으로 만드는 연산. SQL 의 관계 모델 핵심.
종류는 5 가지: INNER, LEFT, RIGHT, FULL OUTER, CROSS. 같은 테이블끼리 결합하는 SELF JOIN 은 별도 종류가 아니라 같은 테이블을 두 번 참조하는 패턴.
pandas 의 Pandas merge 가 완전 대응되는 개념. 시각화는 동일하게 사용한다.
시각화
4 가지 메인 JOIN 타입
| how | SQL | 결과 |
|---|---|---|
| 내부 | INNER JOIN | 양쪽 매칭만 |
| 좌측 외부 | LEFT [OUTER] JOIN | 왼쪽 전부 + 매칭, 없으면 NULL |
| 우측 외부 | RIGHT [OUTER] JOIN | 오른쪽 전부 + 매칭 |
| 완전 외부 | FULL [OUTER] JOIN | 양쪽 합집합, 없으면 NULL |
| 데카르트곱 | CROSS JOIN | 모든 조합 (m × n) |
예제 데이터
-- users 테이블
id | name
----+---------
1 | Alice
2 | Bob
3 | Charlie
-- orders 테이블
user_id | amount
---------+--------
1 | 100
1 | 200
2 | 150
4 | 50-- 이 두 테이블로 4 가지 join 차이를 본다INNER JOIN
양쪽 모두 매칭되는 행만.
SELECT u.id, u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id; id | name | amount
----+-------+--------
1 | Alice | 100
1 | Alice | 200
2 | Bob | 150Alice (2 주문), Bob (1 주문) 만. Charlie 는 주문 없음, orders.user_id=4 는 users 에 없음.
INNER키워드는 생략 가능,JOIN만 쓰면INNER JOIN. 명시 권장.
LEFT JOIN
왼쪽 테이블의 모든 행 + 오른쪽 매칭. 없으면 오른쪽 컬럼은 NULL.
SELECT u.id, u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id; id | name | amount
----+---------+--------
1 | Alice | 100
1 | Alice | 200
2 | Bob | 150
3 | Charlie | NULLCharlie 도 나타난다, amount 는 NULL. “users 전부 + 그들의 주문” 같은 질문에 적합.
LEFT JOIN + IS NULL = 안티 조인
“오른쪽에 없는 왼쪽 행” 찾기.
-- 주문이 한 번도 없는 사용자
SELECT u.id, u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.user_id IS NULL; id | name
----+---------
3 | CharlieNOT EXISTS / NOT IN 으로 같은 결과를 낼 수 있지만, 옵티마이저가 가장 효율적인 plan 을 짜기에 유리한 건 dialect/통계에 따라 다르다.
RIGHT JOIN
LEFT JOIN 의 좌우 반전.
SELECT u.id, u.name, o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
| id | name | amount |
|---|---|---|
| 1 | Alice | 100 |
| 1 | Alice | 200 |
| 2 | Bob | 150 |
| NULL | NULL | 50 |
orders 의 user_id=4 행도 나타난다 (사용자 없음, NULL).
실무에선
RIGHT JOIN보다FROM의 테이블 순서를 바꾸고LEFT JOIN쓰는 게 가독성 좋다.
FULL OUTER JOIN
양쪽 모두 보존, 매칭 안 되면 반대편이 NULL.
SELECT u.id, u.name, o.amount
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
| u.id | name | amount |
|---|---|---|
| 1 | Alice | 100 |
| 1 | Alice | 200 |
| 2 | Bob | 150 |
| 3 | Charlie | NULL |
| NULL | NULL | 50 |
IMPORTANT
MySQL 은 FULL OUTER JOIN 을 직접 지원하지 않는다. LEFT JOIN UNION RIGHT JOIN 으로 흉내.
-- MySQL
SELECT u.id, u.name, o.amount
FROM users u LEFT JOIN orders o ON u.id = o.user_id
UNION
SELECT u.id, u.name, o.amount
FROM users u RIGHT JOIN orders o ON u.id = o.user_id;
CROSS JOIN
모든 조합 (데카르트곱). 행 수 = m × n.
SELECT u.name, c.color
FROM users u
CROSS JOIN colors c;
ON 절이 없다. 보통 의도된 사용은 작은 룩업 테이블 확장, 카탈로그 매트릭스 생성.
CAUTION
큰 두 테이블 사이의 CROSS JOIN 은 즉시 메모리/디스크를 폭발시킨다. 1만 × 1만 = 1억 행.
암시적 cross join (구식):
SELECT u.name, c.color FROM users u, colors c; -- 같은 결과
WHERE 절을 깜빡하면 의도치 않은 cross join 이 된다, 명시적 CROSS JOIN 이 안전.
SELF JOIN
같은 테이블을 두 번 참조. 보통 부모-자식 관계, 동일 도메인 비교.
-- 매니저-부하 관계
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- 같은 city 의 다른 사람
SELECT a.name, b.name AS roommate
FROM users a
JOIN users b
ON a.city = b.city
AND a.id < b.id; -- 중복 쌍 제거 (id 순으로만)
테이블 별칭이 필수.
ON vs USING vs NATURAL JOIN
ON
가장 일반적, 가장 명시적.
JOIN orders o ON u.id = o.user_id
JOIN orders o ON u.id = o.user_id AND o.status = 'active'
USING (양쪽 컬럼 이름 같을 때)
JOIN orders USING (user_id)
-- ON 절의 짧은 버전, 결과 컬럼이 한 번만 나옴
NATURAL JOIN
같은 이름의 모든 컬럼을 자동으로 매칭. 위험하다.
SELECT * FROM users NATURAL JOIN orders;
-- created_at, id 같은 우연히 겹친 컬럼까지 join 키가 됨
스키마가 바뀌면 의미가 조용히 바뀐다. 거의 항상 명시적 ON 권장.
여러 테이블 JOIN
SELECT u.name, o.id, p.name AS product
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE u.city = 'Seoul';
옵티마이저가 join 순서를 선택. 통계 가 정확하면 효율적, 아니면 nested-loop 폭발.
JOIN 알고리즘 (간단)
옵티마이저가 선택하는 물리 알고리즘 3 가지.
| 알고리즘 | 조건 | 비용 |
|---|---|---|
| Nested Loop | 한쪽이 작거나 인덱스 있는 경우 | O(n × m), 인덱스 시 O(n log m) |
| Hash Join | 등호 join, 한쪽이 메모리에 들어감 | O(n + m), build/probe |
| Sort-Merge | 양쪽 정렬됨 또는 정렬 비용 감당 가능 | O(n log n + m log m) |
대용량 hash join 은 External Merge Sort 와 비슷하게 메모리 부족 시 디스크로 spill 한다 (Grace Hash Join). PG/MySQL EXPLAIN ANALYZE 로 확인.
pandas 와 SQL JOIN
Pandas merge 와 1:1 대응.
| SQL | pandas |
|---|---|
INNER JOIN | pd.merge(a, b, how='inner') |
LEFT JOIN | pd.merge(a, b, how='left') |
RIGHT JOIN | pd.merge(a, b, how='right') |
FULL OUTER JOIN | pd.merge(a, b, how='outer') |
CROSS JOIN | pd.merge(a, b, how='cross') |
ON a.x = b.y | pd.merge(a, b, left_on='x', right_on='y') |
USING (k) | pd.merge(a, b, on='k') |
LEFT JOIN ... WHERE b.col IS NULL (anti) | df_a[~df_a['k'].isin(df_b['k'])] |
JOIN ... ON range (PG, 비등호) | pd.merge_asof, IntervalIndex |
pandas 와 SQL 모두 동일한 set-theory 위에서 동작, 위 시각화는 pandas 와 공유한다.
함정
1. 다대다 폭증
users 와 orders 모두 같은 키가 중복돼 있으면 결과 행 수가 곱셈으로 증가.
-- users.id 가 1, 1
-- orders.user_id 가 1, 1, 1
-- → 2 × 3 = 6 행 (의도 아닐 수도)
GROUP BY 또는 서브쿼리로 한쪽 unique 화.
2. NULL 키리는 매칭 안 됨
ON u.col = o.col -- 양쪽 NULL 이어도 매칭 X
ON u.col IS NOT DISTINCT FROM o.col -- PostgreSQL, NULL=NULL 매칭
3. ON 과 WHERE 의 차이 (특히 OUTER JOIN)
-- LEFT JOIN 에서 ON 조건은 결합 단계, WHERE 는 결합 후
SELECT *
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid';
-- Charlie 도 살아 있음 (왼쪽 보존)
SELECT *
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid';
-- Charlie 가 사라짐 (WHERE 가 NULL 행 제거, INNER JOIN 처럼 동작)
OUTER JOIN 에서 우측 컬럼 조건은 ON 에 두는 게 의도와 맞을 때가 많다.
4. SELECT * 의 컬럼 중복
SELECT * FROM users JOIN orders ON users.id = orders.user_id;
-- id 컬럼이 양쪽에서 나옴, 어떤 게 어떤 건지 모호
명시적으로 users.id, orders.id AS order_id.
관련 위키
이 글의 용어 (10개)
- [DB] MySQL / InnoDB: clustered index, redo log, MVCCdatabase-internals
- 정의 MySQL + InnoDB (기본 스토리지 엔진) 의 clustered index + redo log 기반 행 기반 RDBMS. 웹 / SaaS 의 가장 흔한 backbon…
- [DB] PostgreSQL: 프로세스 모델, MVCC, WAL, 확장성database-internals
- 정의 PostgreSQL 은 오픈소스 ORDBMS. 1986 UC Berkeley POSTGRES 의 후예. MVCC, 확장 가능 타입, JSONB, full-text searc…
- [Pandas] concatpandas
- 정의 는 여러 DataFrame/Series 를 세로 (axis=0) 또는 가로 (axis=1) 로 단순 연결. SQL 의 (axis=0) 또는 column-level conca…
- [Pandas] joinpandas
- 정의 는 index 기반 결합 의 편의 API. 내부적으로는 와 동등하지만 더 짧다. 기본 merge 와의 비교 은 index 끼리 매칭이 기본 가정이라 코드가 짧다. 컬럼 기준…
- [Pandas] mergepandas
- 정의 는 두 DataFrame 을 SQL join 처럼 결합. 4 가지 join 타입 (inner / left / right / outer) 지원. 시각화 4 가지 join 타입…
- [SQL] Data Typessql
- 정의 SQL 의 컬럼은 타입 을 가진다. 가능한 값의 도메인, 가능한 연산, 정렬 순서, 인덱스 종류, 저장 크기가 타입에 따라 결정된다. 같은 의미의 타입도 dialect 마다…
- [SQL] DMLsql
- 정의 DML (Data Manipulation Language) 은 데이터 자체를 조회/삽입/수정/삭제 하는 SQL 의 하위 집합. 일상 쿼리의 99% 가 여기에 속한다. | 명…
- [SQL] GROUP BYsql
- 정의 는 데이터를 키별로 그룹으로 나누고 각 그룹에 집계 함수 (aggregate) 를 적용해 그룹당 한 행으로 줄이는 연산. SQL 의 split-apply-combine. 와…
- External Merge Sortalgorithm
- 정의 External Merge Sort (외부 머지 정렬) 는 데이터가 메모리에 들어가지 않을 때 사용하는 변형. 두 단계로 동작한다. 1. Run 생성: 메모리 크기만큼 청크…
- SQLsql
- 정의 SQL (Structured Query Language) 은 관계형 데이터베이스 (RDBMS) 와 대화하기 위한 선언형 (declarative) 질의 언어. 1970 년 E…
💬 댓글