해시조인 “문제 상황” 퍼이썬으로 재현
- 두 가지 시나리오를 비교
- - 시나리오 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 현상과의 연관성
- Python 시뮬레이션의 “버킷 부족/스큐 → max_chain↑ → probe_time↑”는
- - Oracle의 “키 왜곡으로 특정 버킷 과밀” 또는 “메모리 부족으로 배치/스필 발생” 시, 버킷 체인 스캔과 추가 I/O로 응답시간이 늘어나는 것과 같은 맥락입니다.
- 실전 튜닝 체크리스트
- - 빌드/프로브 선택 검토(SWAP_JOIN_INPUTS), 작은 쪽을 빌드로.
- - 조인 키 히스토그램으로 스큐 인지, 통계 최신화.
- - PGA/동시성 조정으로 스필 최소화(1-pass/multipass 회피).
- - 병렬 시 분배 전략 조정(작은 테이블 BROADCAST, 큰 테이블 HASH 등).
- - 심한 핫 키는 설계/SQL에서 SALT 기법으로 분산.
- - 상황에 따라 USE_NL/USE_MERGE로 방식 전환.
menu_book [결론]
- Hash Bucket은 조인 키를 해시 영역으로 나누는 인덱스 역할을 하며, 균형 잡힌 버킷과 충분한 메모리가 성능의 핵심입니다.
- 버킷 과밀과 키 왜곡은 체인 스캔 비용을 키워 성능을 크게 떨어뜨립니다.
- 위 파이썬 코드는 이러한 병목을 실험적으로 확인하도록 도와줍니다.