Blog
pysparksparkjoinsemi-joinanti-joindata-engineering

PySpark 조인 완전 마스터 — semi, anti, null-safe, 그리고 함정들

inner/outer 를 넘어 semi·anti join 으로 필터링을 효율화하고, null-safe 조인으로 NULL 키 버그를 잡고, 다중 키·비등가 조인·중복 폭발·컬럼 모호성 같은 실전 함정을 피하는 법을 PySpark 코드로 정리합니다.

Data Dynamics2026년 6월 5일9 min read

조인은 데이터 처리의 절반입니다. 그런데 많은 사람이 inner/left 만 쓰고, semi/anti 의 효율도, null-safe 조인의 필요성도 모른 채 미묘한 버그와 성능 문제를 만듭니다. "존재 여부만 확인하려고 조인했더니 행이 두 배가 됐다"거나 "NULL 키 때문에 결과가 틀어졌다" 같은 일이 흔합니다.

이 글은 PySpark 의 모든 조인 타입을 용도별로 정리하고, 실전에서 자주 부딪히는 함정 — 중복 폭발, 컬럼 모호성, NULL 키 — 을 피하는 법을 다룹니다.

1. 조인 타입 전체 지도

a.join(b, "key", how="inner")   # 기본
how반환용도
inner양쪽 매칭일반 조인
left(left_outer)왼쪽 전부 + 매칭보강(enrichment)
right오른쪽 전부 + 매칭(드묾)
full(full_outer)양쪽 전부양쪽 비교
left_semi왼쪽 중 매칭되는 행만존재 필터(EXISTS)
left_anti왼쪽 중 매칭 안 되는 행만부재 필터(NOT EXISTS)
cross카테시안 곱(위험, 명시 필요)

2. Semi Join — 존재 확인을 효율적으로

"주문이 있는 고객만"처럼 존재 여부로 필터할 때, inner join 을 쓰면 오른쪽 컬럼이 붙고 중복이 생깁니다(한 고객이 주문 5건이면 5행). left_semi 는 왼쪽 행만, 중복 없이, 매칭되는 것만 반환합니다.

# ❌ inner join: 주문 수만큼 고객이 중복됨 + 불필요한 컬럼
customers.join(orders, "cust_id")   # 고객이 주문 건수만큼 늘어남
 
# ✅ left_semi: 주문 있는 고객만, 중복 없이, 고객 컬럼만
customers.join(orders, "cust_id", "left_semi")
left_semi = "오른쪽에 매칭이 존재하는 왼쪽 행" (오른쪽 컬럼 안 붙음, 중복 없음)

SQL 의 WHERE EXISTS (...) 와 같고, IN 서브쿼리보다 명확하고 효율적입니다.

3. Anti Join — 부재 확인 (차집합)

"주문이 없는 고객", "마스터에 없는 신규 키" — 부재 필터left_anti 가 정답입니다.

# 주문 이력이 없는 고객
inactive = customers.join(orders, "cust_id", "left_anti")
 
# 신규 키 찾기 (타깃에 아직 없는 소스 행) — 백필·증분에 유용
new_records = source.join(target, "id", "left_anti")
left_anti = "오른쪽에 매칭이 없는 왼쪽 행" = 차집합(왼쪽 - 오른쪽)

anti join 은 백필·증분 적재에서 "아직 없는 것만 추리기"에 자주 쓰입니다(별도 글 "PySpark 대규모 중복 제거와 SCD Type 2"의 신규 키 판별).

목적안티패턴정답
존재하는 것만inner(중복)left_semi
없는 것만left join + null 필터left_anti

4. 함정 ① NULL 키 — null-safe 조인

일반 조인에서 NULL = NULL 은 매칭되지 않습니다(SQL 표준). NULL 키끼리 붙어야 하는 경우(또는 NULL 을 키로 다룰 때) 버그가 생깁니다.

# 일반 조인: NULL 키는 매칭 안 됨 (의도와 다를 수 있음)
a.join(b, a.k == b.k)
 
# null-safe 조인: NULL = NULL 을 매칭으로 취급 (<=> 연산자)
a.join(b, a["k"].eqNullSafe(b["k"]))

eqNullSafe(<=>)는 NULL 끼리도 매칭합니다. 단, NULL 키가 많으면 그 자체가 스큐를 유발하므로(별도 글 "PySpark 데이터 스큐 완전 정복"), 보통은 조인 전에 NULL 키를 분리·처리하는 편이 안전합니다.

5. 함정 ② 중복 폭발 (Fan-out)

조인 키가 한쪽에서 유일하지 않으면, 행이 곱으로 늘어납니다. 양쪽 다 키 중복이 있으면 폭발합니다.

a: key=1 이 3행,  b: key=1 이 4행  →  조인 결과 key=1 이 12행 💥
# 조인 전에 한쪽을 키당 1행으로 보장 (의도한 게 아니라면)
b_unique = b.dropDuplicates(["key"])   # 또는 window 로 대표행 선택
a.join(b_unique, "key")

흔한 사고: 차원 테이블에 중복이 있는 줄 모르고 조인 → 팩트가 부풀어 집계가 틀림. 조인 전 양쪽의 키 유일성을 확인하는 습관이 중요합니다.

6. 함정 ③ 컬럼 모호성 (Ambiguous Column)

같은 이름의 컬럼이 양쪽에 있으면 조인 후 모호성 에러가 납니다.

# ❌ 모호: 양쪽에 amount 가 있으면 어느 쪽인지 모호
a.join(b, "key").select("amount")   # AnalysisException 가능
 
# ✅ 방법 1: 동등 키는 문자열/리스트로 조인하면 키 컬럼이 하나로 합쳐짐
a.join(b, ["key"])
 
# ✅ 방법 2: alias 로 명확히
a.alias("a").join(b.alias("b"), "key").select("a.amount", "b.amount")
 
# ✅ 방법 3: 조인 전에 미리 rename
b2 = b.withColumnRenamed("amount", "b_amount")

join(b, "key")(문자열/리스트 키)는 키 컬럼을 하나로 합쳐 모호성을 줄입니다. a.k == b.k(표현식)는 양쪽 키 컬럼이 모두 남습니다.

7. 다중 키·비등가(non-equi) 조인

# 다중 키
a.join(b, ["key1", "key2"])
 
# 비등가 조인 (범위) — 셔플·폭발 주의
a.join(b, (a.ts >= b.valid_from) & (a.ts < b.valid_to))

비등가(범위) 조인은 등가 조인보다 훨씬 비쌉니다(broadcast 가능하지 않으면 거의 cross 에 가까움). 유효기간 조인 같은 범위 조인은 as-of 패턴으로 전환을 고려하세요(별도 글 "PySpark As-of Join").

8. Cross Join — 명시적으로만

카테시안 곱은 실수로 만들면 재앙입니다. Spark 는 기본적으로 의도치 않은 cross join 을 막습니다.

# 의도적 cross join 은 명시
a.crossJoin(b)
 
# 실수 방지 설정 (기본 false 권장 — 실수성 cross 차단)
spark.conf.set("spark.sql.crossJoin.enabled", "false")

9. 조인 성능 — 분배 방식

조인 타입과 별개로, 어떻게 분배되는가(broadcast vs sort-merge)가 성능을 좌우합니다.

상황전략참고
한쪽 작음broadcast"Broadcast 변수와 대형 Lookup"
양쪽 큼·반복버킷팅"PySpark 버킷팅"
키 스큐salt/AQE"PySpark 데이터 스큐"
큰 테이블 조인dynamic filtering(CBO)

semi/anti join 도 내부적으로 broadcast/sort-merge 로 실행되므로 같은 최적화가 적용됩니다.

10. 정리

목적조인
일반 매칭inner
보강left
존재 필터(중복 없이)left_semi
부재 필터(차집합)left_anti
NULL 키 매칭eqNullSafe
양쪽 비교full_outer

조인 마스터의 핵심은 "의도에 맞는 조인 타입을 고르는 것"입니다. 존재 확인엔 semi, 부재 확인엔 anti 를 쓰면 중복 폭발과 불필요한 컬럼을 피하고, null-safe 로 NULL 키 버그를 막으며, 조인 전 키 유일성 확인으로 fan-out 을 예방합니다. inner/left 만 쓰던 습관에서 벗어나 semi/anti 를 손에 익히면, 조인 코드가 더 정확하고 효율적이 됩니다.


이 글은 Spark 3.5 기준으로 작성되었습니다. 복잡한 조인 로직·성능 최적화 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀