메뉴 여닫기
개인 메뉴 토글
로그인하지 않음
만약 지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
Oracle (토론 | 기여)님의 2025년 8월 8일 (금) 14:31 판
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)

해시조인 “문제 상황” 퍼이썬으로 재현

  • 두 가지 시나리오를 비교
    - 시나리오 A(정상): 적정 버킷 수 + 고르게 분포된 키
    - 시나리오 B(문제): 너무 적은 버킷 수 + 편향된 키(핫 키 집중)
  • 핵심 포인트는 “버킷 과밀 → 체인 길이 급증 → 조인 탐색 시간 증가”를 눈으로 확인하는 것입니다.
import random
import time
from collections import defaultdict, Counter

random.seed(42)

class HashTable:
    def __init__(self, num_buckets: int):
        self.num_buckets = num_buckets
        # 각 버킷은 (key, payload) 튜플 리스트
        self.buckets = [[] for _ in range(num_buckets)]
        self.rows = 0
        self.collisions = 0
        self.max_chain = 0

    def _bucket_idx(self, key):
        # 파이썬 내장 해시를 사용하되, 버킷 수로 모듈로
        return hash(key) % self.num_buckets

    def insert(self, key, payload):
        idx = self._bucket_idx(key)
        bucket = self.buckets[idx]
        if bucket:  # 버킷에 이미 뭔가 있으면 충돌로 카운트
            self.collisions += 1
        bucket.append((key, payload))
        self.rows += 1
        if len(bucket) > self.max_chain:
            self.max_chain = len(bucket)

    def probe(self, key):
        idx = self._bucket_idx(key)
        bucket = self.buckets[idx]
        # 동일 키만 반환 (동등 조인)
        return [p for (k, p) in bucket if k == key]

def generate_data(n_build=50_000, n_probe=150_000, skew=False):
    """
    빌드/프로브 양쪽에 키와 페이로드를 생성.
    skew=True면 '핫 키'가 대량으로 생성되어 특정 버킷에 몰리게 됨.
    """
    build = []
    probe = []

    if not skew:
        # 고른 분포: 넓은 키 도메인에서 균일 랜덤
        domain = n_build * 20
        build_keys = [random.randint(0, domain) for _ in range(n_build)]
        probe_keys = [random.randint(0, domain) for _ in range(n_probe)]
    else:
        # 편향된 분포: 80%는 작은 도메인(핫 키), 20%는 넓은 도메인
        hot_ratio = 0.8
        hot_domain = max(10, n_build // 200)   # 매우 작은 도메인
        cold_domain = n_build * 20

        def skewed_keys(n):
            keys = []
            for _ in range(n):
                if random.random() < hot_ratio:
                    keys.append(random.randint(0, hot_domain))  # 핫 영역
                else:
                    keys.append(random.randint(hot_domain + 1, hot_domain + cold_domain))
            return keys

        build_keys = skewed_keys(n_build)
        probe_keys = skewed_keys(n_probe)

    # 페이로드는 단순 숫자/문자열
    build = [(k, f"B{k}") for k in build_keys]
    probe = [(k, f"P{k}") for k in probe_keys]
    return build, probe

def hash_join(build_rows, probe_rows, num_buckets):
    ht = HashTable(num_buckets)
    t0 = time.perf_counter()
    for k, p in build_rows:
        ht.insert(k, p)
    t1 = time.perf_counter()

    matches = 0
    for k, _ in probe_rows:
        matches += len(ht.probe(k))
    t2 = time.perf_counter()

    stats = {
        "num_buckets": num_buckets,
        "build_rows": len(build_rows),
        "probe_rows": len(probe_rows),
        "collisions": ht.collisions,
        "max_chain": ht.max_chain,
        "avg_rows_per_bucket": round(ht.rows / num_buckets, 2),
        "build_time_ms": round((t1 - t0) * 1000, 2),
        "probe_time_ms": round((t2 - t1) * 1000, 2),
        "total_time_ms": round((t2 - t0) * 1000, 2),
        "matches": matches
    }
    return stats

def run_scenarios():
    n_build = 50_000
    n_probe = 150_000

    print("=== 시나리오 A: 균일 분포 + 충분한 버킷 ===")
    buildA, probeA = generate_data(n_build, n_probe, skew=False)
    statsA = hash_join(buildA, probeA, num_buckets=65_536)  # 충분한 버킷
    for k, v in statsA.items():
        print(f"{k}: {v}")

    print("\n=== 시나리오 B: 편향 분포 + 부족한 버킷(문제 상황) ===")
    buildB, probeB = generate_data(n_build, n_probe, skew=True)
    statsB = hash_join(buildB, probeB, num_buckets=512)  # 의도적으로 작은 버킷
    for k, v in statsB.items():
        print(f"{k}: {v}")

    # 간단한 비교 요약
    print("\n=== 비교 요약 ===")
    print(f"버킷 충분(A) vs 부족(B) - max_chain: {statsA['max_chain']} vs {statsB['max_chain']}")
    print(f"버킷 충분(A) vs 부족(B) - probe_time_ms: {statsA['probe_time_ms']} vs {statsB['probe_time_ms']}")
    print("체인 길이와 프로브 시간의 상관 관계를 확인해 보세요.")

if __name__ == "__main__":
    run_scenarios()

예상 시나리오

1) 시나리오 A(균일+버킷 충분): max_chain이 상대적으로 작고, probe_time이 짧습니다. 2) 시나리오 B(편향+버킷 부족): collisions와 max_chain이 커지고, probe_time이 유의미하게 늘어납니다.

  • 이것이 Oracle에서 키 왜곡과 버킷 과밀로 인해 체인 스캔 비용이 증가하는 현상의 축약판입니다.
  • 실제 Oracle은 더 정교한 해시/배치/스필 전략을 사용하지만, 기본 병목(충돌→체인 스캔→CPU↑/I/O↑)은 동일합니다.


Oracle 현상과의 연관성

  1. Python 시뮬레이션의 “버킷 부족/스큐 → max_chain↑ → probe_time↑”는
    - Oracle의 “키 왜곡으로 특정 버킷 과밀” 또는 “메모리 부족으로 배치/스필 발생” 시, 버킷 체인 스캔과 추가 I/O로 응답시간이 늘어나는 것과 같은 맥락입니다.
  2. 실전 튜닝 체크리스트
    - 빌드/프로브 선택 검토(SWAP_JOIN_INPUTS), 작은 쪽을 빌드로.
    - 조인 키 히스토그램으로 스큐 인지, 통계 최신화.
    - PGA/동시성 조정으로 스필 최소화(1-pass/multipass 회피).
    - 병렬 시 분배 전략 조정(작은 테이블 BROADCAST, 큰 테이블 HASH 등).
    - 심한 핫 키는 설계/SQL에서 SALT 기법으로 분산.
    - 상황에 따라 USE_NL/USE_MERGE로 방식 전환.


menu_book [결론]
  • Hash Bucket은 조인 키를 해시 영역으로 나누는 인덱스 역할을 하며, 균형 잡힌 버킷과 충분한 메모리가 성능의 핵심입니다.
  • 버킷 과밀과 키 왜곡은 체인 스캔 비용을 키워 성능을 크게 떨어뜨립니다.
  • 위 파이썬 코드는 이러한 병목을 실험적으로 확인하도록 도와줍니다.