해시조인 “문제 상황” 퍼이썬으로 재현
- 두 가지 시나리오를 비교
- - 시나리오 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은 조인 키를 해시 영역으로 나누는 인덱스 역할을 하며, 균형 잡힌 버킷과 충분한 메모리가 성능의 핵심입니다.
- 버킷 과밀과 키 왜곡은 체인 스캔 비용을 키워 성능을 크게 떨어뜨립니다.
- 위 파이썬 코드는 이러한 병목을 실험적으로 확인하도록 도와줍니다.