메뉴 여닫기
개인 메뉴 토글
로그인하지 않음
만약 지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

세미 조인(Semi Join)

목적

첫 번째 테이블의 데이터 중 두 번째 테이블과 일치하는 데이터만 반환

특징

  • 조인 조건에 만족하는 첫 번째 테이블의 행만 선택
  • 실제 두 번째 테이블의 컬럼은 결과에 포함되지 않음

주요 구현 방법

  • EXISTS 연산자 사용
  • IN 연산자 사용
  • 예시 쿼리:
SELECT e.employee_id, e.name
FROM employees e
WHERE EXISTS (
    SELECT 1 
    FROM departments d 
    WHERE e.department_id = d.department_id
);

세미조인 알고리즘을 파이썬으로 구현

  1. 세미 조인은 "왼쪽(주 테이블)의 행 중에서 오른쪽(참조 테이블)에 일치하는 행이 존재하는지 여부만 검사"하여,
  2. 일치하는 왼쪽 행만 결과로 반환합니다.
  3. 오른쪽 테이블의 컬럼 값은 결과에 포함되지 않습니다.

프로그램 로직 설명

  1. 관계형 테이블을 Python의 리스트(딕셔너리 항목 리스트)로 표현하고,
  2. 조인 조건을 함수로 넘겨서 세미 조인을 수행하는 예시
  3. 또한 성능 관점에서 해시 기반(semi-join using hash set) 알고리즘과 중복 일치 처리(첫 발견 시 멈춤)를 반영

사용법 요약

  1. left_rows, right_rows: 각 행은 컬럼명을 키로 하는 dict.
  2. join_keys: 왼쪽과 오른쪽에서 비교할 키 이름의 튜플 리스트. 예: [("dept_id", "dept_id")]
  3. key_func: (선택) 복잡한 조건이 필요하면 행 -> 키 변환 함수로 대체 가능.
  4. semi_join_hash(left_rows, right_rows, join_keys) 호출로 결과 얻음.

- 코드:

from typing import List, Dict, Tuple, Callable, Any, Iterable

Row = Dict[str, Any]

def make_key_from_row(row: Row, keys: Iterable[str]) -> Tuple:
    """주어진 키 목록으로부터 튜플 키를 생성합니다. None 허용."""
    return tuple(row.get(k) for k in keys)

def semi_join_hash(
    left_rows: List[Row],
    right_rows: List[Row],
    join_keys: List[Tuple[str, str]],
    right_key_func: Callable[[Row], Tuple] = None,
    left_key_func: Callable[[Row], Tuple] = None
) -> List[Row]:
    """
    해시 기반 세미 조인 구현.

    매개변수:
    - left_rows: 왼쪽 테이블 행 리스트 (결과로 반환될 테이블)
    - right_rows: 오른쪽 테이블 행 리스트 (매칭 존재 여부 확인용)
    - join_keys: 왼쪽-오른쪽 키 쌍 리스트, 예: [("dept_id", "dept_id"), ("emp_no","emp_no")]
    - right_key_func: (선택) 오른쪽 행 -> 키 튜플 생성 함수. 지정되면 join_keys 무시 가능.
    - left_key_func: (선택) 왼쪽 행 -> 키 튜플 생성 함수. 지정되면 join_keys 무시 가능.

    반환값:
    - left_rows 중 오른쪽에 매칭되는 행만 포함하는 리스트
    """
    if right_key_func is None:
        right_key_names = [rk for _, rk in join_keys]
        def right_key_func(r): return make_key_from_row(r, right_key_names)

    if left_key_func is None:
        left_key_names = [lk for lk, _ in join_keys]
        def left_key_func(l): return make_key_from_row(l, left_key_names)

    # 1) 오른쪽 테이블의 키 집합 생성 (해시)
    right_key_set = set()
    for r in right_rows:
        key = right_key_func(r)
        # Oracle 세미조인 관점: NULL 포함 키 처리 시 NULL이 포함된 키는 매칭으로 간주하지 않는다.
        # Python에서는 None을 포함한 튜플도 set에 들어가므로, NULL 기준을 따르고 싶다면 아래처럼 필터링 가능.
        if any(v is None for v in key):
            # NULL 키는 매칭 불가(옵션). 필요 시 주석 처리하여 NULL도 허용 가능.
            continue
        right_key_set.add(key)

    # 2) 왼쪽 테이블을 순회하며 오른쪽 키 존재 여부만 검사
    result = []
    for l in left_rows:
        lkey = left_key_func(l)
        if any(v is None for v in lkey):
            # 왼쪽 키에 NULL이 있으면 일반적으로 매칭 불가. 필요 시 로직 변경 가능.
            continue
        if lkey in right_key_set:
            result.append(l)

    return result

사용 예시


left = [
    {"emp_id": 1, "name": "Kim", "dept_id": 10},
    {"emp_id": 2, "name": "Lee", "dept_id": 20},
    {"emp_id": 3, "name": "Park", "dept_id": None},
    {"emp_id": 4, "name": "Choi", "dept_id": 30},
]

right = [
    {"dept_id": 10, "dept_name": "HR"},
    {"dept_id": 20, "dept_name": "Sales"},
    {"dept_id": None, "dept_name": "Unknown"},
]

joined = semi_join_hash(left, right, [("dept_id", "dept_id")])
# joined 결과: emp_id 1, 2만 포함 (dept_id None은 무시)
print(joined)


  • 주의사항 및 확장 아이디어:
- NULL 처리: SQL과 Python의 NULL(None) 동작 차이를 유의해야 합니다. 위 구현은 키에 None이 포함되면 매칭되지 않도록 처리했습니다. 필요하면 None 허용 방식으로 변경하실 수 있습니다.
- 다중 컬럼 조인: join_keys에 여러 쌍을 넣으면 다중 컬럼 조인이 됩니다.
- 대량 데이터: 메모리 제약이 있다면 해시 대신 블록 기반 또는 외부 정렬(merge) 접근을 구현해야 합니다.
- Anti Join: 동일한 원리로 right_key_set에 없는 left 행을 반환하도록 바꾸면 안티 조인이 됩니다.
- EXISTS의 짧은 회로(semi-join short-circuit) 동작을 흉내내려면 right 테이블에 대해 인덱스 또는 키 집합을 먼저 만든 후 왼쪽을 순회하는 현재 방법이 적절합니다.