Outer Join , other join & Experiments
Outer Join & 기타 Join
요약
외부 조인(OUTER JOIN) 은 한쪽 테이블에는 데이터가 있지만, 다른 쪽에는 없는 데이터도 결과에 포함시킨다.
기준이 되는 테이블을 왼쪽으로 두면
LEFT JOIN, 오른쪽으로 두면RIGHT JOIN.LEFT JOIN: 왼쪽 테이블의 모든 행을 포함하고, 오른쪽은 조건에 맞는 데이터만 붙임. 없으면 NULL.
RIGHT JOIN: 오른쪽 테이블의 모든 행을 포함하고, 왼쪽은 조건에 맞는 데이터만 붙임. 없으면 NULL.
FULL OUTER JOIN: 양쪽 모두의 모든 행을 포함. (MySQL은 지원하지 않음.)
SELF JOIN: 같은 테이블을 스스로 조인하여 계층 구조나 관계를 표현.
CROSS JOIN: 두 테이블의 모든 행 조합(Cartesian product)을 반환. 필터가 없으면 결과 행 수가 폭발적으로 증가.
실무에서는
LEFT JOIN이 압도적으로 많이 사용됨.RIGHT JOIN은 테이블 순서만 바꿔도 LEFT로 대체 가능.
1. 외부 조인 개념
내부 조인(INNER JOIN)은 양쪽에 모두 존재하는 데이터만 보여줌(교집합). 하지만 때로는 한쪽에 있는 데이터도 결과에 포함하고 싶을때가 있음.
예:
가입했지만 주문하지 않은 고객
등록했지만 한번도 팔리지 않은 재품
내부 조인으로는 이런 데이터가 아예 누락되어있으므로, 외부 조인을 사용해야 함.
1-1. LEFT JOIN
동작 방식
FROM 절 왼쪽에 있는 테이블이 기준.
왼쪽의 모든 데이터를 포함하고,
ON조건에 맞는 오른쪽 데이터를 붙임.오른쪽에 매칭이 없으면 해당 칼럼은
NULL.
SELECT u.user_id, u.name, o.order_id
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;모든 고객이 출력됨.
주문이 없는 고객은
order_id가NULL.
사용 예시
가입했지만 주문하지 않은 고객 찾기:
SELECT u.user_id, u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;1-2. RIGHT JOIN
동작 방식
JOIN 절 오른쪽에 있는 테이블이 기준.
오른쪽의 모든 데이터를 포함하고, 왼쪽에서 매칭되는 데이터를 붙임.
매칭이 없으면
NULL.
SELECT o.order_id, p.name
FROM orders o
RIGHT JOIN products p ON o.product_id = p.product_id;특징
LEFT JOIN과 결과는 같지만 기준 테이블 위치만 다름.
실무에서는 거의 안 쓰고, LEFT JOIN으로 변환하는 경우가 많음.
1-3. FULL OUTER JOIN
양쪽 테이블의 모든 데이터를 포함.
매칭이 없으면 한쪽 컬럼은 NULL.
MySQL에서는 직접 지원하지 않음 (UNION으로 구현 가능).
SELECT ... FROM A
LEFT JOIN B ON ...
UNION
SELECT ... FROM A
RIGHT JOIN B ON ...1-4. 논리적 개념
LEFT JOIN: FROM 절에 있는 테이블(왼쪽)이 기준.
왼쪽 테이블의 모든 데이터를 결과에 포함.
ON 조건에 맞는 데이터를 오른쪽 테이블에서 찾아 붙임.
매칭이 없으면 NULL 채움.
RIGHT JOIN: JOIN 절에 있는 테이블(오른쪽)이 기준.
오른쪽 테이블의 모든 데이터를 결과에 포함.
ON 조건에 맞는 데이터를 왼쪽 테이블에서 찾아 붙임.
매칭이 없으면 NULL 채움.
FULL OUTER JOIN: 양쪽 모두의 데이터를 포함. 교집합 + 각 집합의 단독 데이터. (MySQL은 지원하지 않음)
1-5. 물리적 개념
내부 조인과 동일하게 Nested Loop / Hash Join / Merge Join 방식 사용 가능.
차이점은 조인 후 필터링 단계에서 NULL 값을 허용하는지 여부.
예: LEFT JOIN에서는 왼쪽 테이블이 외부 루프, 오른쪽 테이블이 내부 루프 역할을 하며 NULL 채우기를 수행.
1-6. 실행 흐름 예시
LEFT JOIN으로 한 번도 주문하지 않은 고객 찾기
왼쪽 테이블(users) 전 행 포함.
orders에서 user_id 일치 데이터 검색.
매칭이 없으면 NULL.
WHERE o.order_id IS NULL로 필터링.
LEFT JOIN으로 한 번도 팔리지 않은 상품 찾기
기준: products
LEFT JOIN → 주문 없는 상품의 주문 컬럼은 NULL → WHERE 조건으로 필터.
RIGHT JOIN 동일 예시
테이블 순서 바꾸고 RIGHT JOIN으로도 같은 결과 가능.
실무에서는 LEFT JOIN이 가독성이 좋아 더 많이 사용됨.
2. SELF JOIN
개념
동일한 테이블을 자기 자신과 조인.
보통 계층 구조(트리)나 상하 관계 표현에 사용.
한 테이블을 서로 다른 별칭(A, B)로 두어 부모-자식 관계 연결.
예시
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;직원 테이블을 두 번 사용.
직원의 상사 이름 조회 가능.
3. CROSS JOIN
개념
두 테이블의 모든 행을 조합 (데카르트 곱).
ON 조건 없이 실행.
m × n 개의 결과 생성.
상품 옵션 조합, 테스트 데이터 생성 등에 사용.
예시
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;모든 사이즈와 색상 조합 반환.
4. 조인 시 행 개수 변화 특징
자식 → 부모 조인 (FK → PK): 행 수 그대로.
부모 → 자식 조인 (PK → FK): 자식의 수만큼 행 증가.
SELF JOIN: 매칭 수에 따라 증가 가능.
CROSS JOIN: 항상 곱셈 만큼 행 수 폭증.
궁금한 점
Q. LEFT JOIN과 RIGHT JOIN은 단순히 방향만 다른가?
A. 옵티마이저가 조인 순서를 자동으로 바꾸기도 하지만, 최선의 선택은 아님. 여러 테이블을 조인할 때는 초기에 건수를 가장 많이 줄일 수 있는 테이블을 먼저 드라이빙 테이블로 잡는게 좋다.
정리:
상황 1: 인덱스 X, WHERE X
Observation
두 쿼리 모두 해시 기반 조인(BNL/Hash Join) 경로로 갔고(
Using join buffer (hash join)),LEFT(o→u→p)와RIGHT(p→o→u)의 실행 시간이 유사.o/u/p가 모두 풀 스캔으로 해시 빌드/프로브를 수행.
Reason
필터가 없고 인덱스를 막았기 때문에, 어느 쪽을 먼저 읽어도 결과 행수(=50만)와 필요한 비교량이 비슷함.
해시 조인은 “읽은 만큼 해시 테이블을 만들고 매칭”하므로, 조인 순서보다 입력 크기가 지배.
Practice
인덱스가 전혀 없고 필터도 없으면 조인 순서로 큰 차이를 만들기 어렵다. 이 구간에서의 최적화는 join_buffer_size(세션) 튜닝이나 배치/병렬화가 더 효과적.
해시/BNL 경로를 확실히 관찰하고 싶으면
IGNORE INDEX+EXPLAIN ANALYZE로 재현 가능.
상황 2: 인덱스 O, WHERE X
Observation
LEFT(orders 드라이빙):orders풀스캔 +users PK/products PK단건 점프 → Nested Loop로 50만 루프.RIGHT(products 드라이빙):products풀스캔 +orders(product_id)범위 조회(평균 ~100건) +users PK점프 → 결국 총 점프량이 비슷해서 시간도 비슷.
Reason
어느 방향이든 최종 결과는 항상 50만 행이고, 조인 키에 적절한 인덱스가 모두 존재하므로 랜덤 접근 비용이 상쇄되어 체감 차이가 작음.
Practice
WHERE가 없고 조인 키 인덱스가 양쪽에 잘 있으면 조인 순서의 체감 이득은 작다.
상황 3: 인덱스 O, WHERE O (o.status + o.order_date)
Observation
LEFT(orders 먼저):(status, order_date)인덱스 범위 스캔으로 초기에 11,111행으로 급감 → 이후 PK 점프 → ~64ms 수준.RIGHT(products 먼저):products를 먼저 스캔하고orders(product_id)를 찾아 나중에 상태/기간 필터 → ~328ms, 5배 이상 느림.
Reason
**선택도 높은 필터가 걸리는 테이블(orders)**을 드라이빙으로 잡으면 초기에 행 수를 크게 줄여 이후 조인 비용이 폭감.
RIGHT경로는 필터가 사후 적용되어 불필요 접근이 많이 발생.
Practice
규칙: “가장 잘 거르는 테이블(선택도 高) → 드라이빙”.
해당 테이블에 복합 인덱스의 선두를 필터 컬럼 순서로 둔다. 예) 이번 케이스:
(status, order_date)→ 범위 후 조인 키로 점프가 쉬움.옵티마이저가 순서를 바꿔 비효율을 택할 때는
STRAIGHT_JOIN/힌트(JOIN_ORDER,NO_BNL등, 버전 의존)로 순서 고정 실험.
요약
인덱스 X, WHERE X: 조인 순서보다 입력 크기가 지배 → 해시/BNL 경로에서 차이 미미.
인덱스 ○, WHERE X: 조인 키 인덱스가 양쪽에 있으면 순서 차이는 작다.
인덱스 ○, WHERE ○: 선택도 높은 조건이 걸리는 테이블을 드라이빙으로 잡으면 결정적 차이가 난다(이번 실험에서 ~5×).
Q. LIKE '검색어%'와 LIKE '%검색어%'의 성능 차이는 얼마나 날까?
LIKE '검색어%'와 LIKE '%검색어%'의 성능 차이는 얼마나 날까?A. LIKE '검색어%'는 인덱스 사용으로 빠르게 동작하지만, LIKE '%검색어%'는 인덱스 미사용으로 풀스캔이 발생해 큰 성능 차이가 납니다.
-- 접두사: 인덱스 범위 스캔 기대 (UNIQUE(email) 활용)
EXPLAIN ANALYZE
SELECT SQL_NO_CACHE COUNT(*) -- 출력 최소화
FROM test_users
WHERE email LIKE 'user1%';
-> Aggregate: count(0) (cost=7981 rows=1) (actual time=6.62..6.62 rows=1 loops=1)
-> Filter: (test_users.email like 'user1%') (cost=5572 rows=24094) (actual time=0.0791..6.1 rows=11111 loops=1)
-> Covering index range scan on test_users using email over ('user1' <= email <= 'user1????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????') (cost=5572 rows=24094) (actual time=0.074..4.71 rows=11111 loops=1)
-- 포함: 좌측 와일드카드로 인해 인덱스 사용 불가 → 풀스캔 예상
EXPLAIN ANALYZE
SELECT SQL_NO_CACHE COUNT(*)
FROM test_users
WHERE email LIKE '%example%';
-> Aggregate: count(0) (cost=5625 rows=1) (actual time=25.2..25.2 rows=1 loops=1)
-> Filter: (test_users.email like '%exam%') (cost=5068 rows=5568) (actual time=0.0609..23 rows=50000 loops=1)
-> Covering index scan on test_users using email (cost=5068 rows=50116) (actual time=0.0574..11.1 rows=50000 loops=1)
Last updated





