JOIN 최소화 및 최적화하기

JOIN 최소화 및 최적화하기

JOIN은 단일 역정규화된 테이블에서 조회하는 것보다 본질적으로 비용이 더 듭니다. 이 문서에서는 언제 역정규화를 해야 하는지, JOIN이 꼭 필요할 때 어떤 모범 사례를 따라야 하는지, 그리고 올바른 JOIN 알고리즘을 어떻게 고르는지 설명할게요.

출처: 문서

본문

ClickHouse는 다양한 JOIN 타입과 알고리즘을 지원하며, 최근 릴리스에서 JOIN 성능이 크게 개선되었습니다. 하지만 JOIN은 단일 역정규화된 테이블에서 조회하는 것보다 본질적으로 더 비쌉니다. 역정규화는 계산 작업을 쿼리 시점에서 삽입 또는 전처리 시점으로 옮기며, 결과적으로 실행 시 지연 시간이 크게 줄어드는 경우가 많습니다. 실시간 또는 지연 시간에 민감한 분석 쿼리에는 역정규화를 강력히 권장합니다. 일반적으로 다음 경우에 역정규화하세요:

  • 테이블이 자주 바뀌지 않거나 배치 새로고침이 허용될 때.
  • 관계가 다대다(many-to-many)가 아니거나 카디널리티가 지나치게 높지 않을 때.
  • 쿼리할 컬럼의 제한된 하위 집합만 있을 때, 즉 일부 컬럼은 역정규화에서 제외할 수 있을 때.
  • 실시간 보강(enrichment)이나 평탄화를 관리할 수 있는 Flink 같은 업스트림 시스템으로 처리를 ClickHouse 밖으로 옮길 능력이 있을 때.

모든 데이터를 역정규화할 필요는 없습니다. 자주 조회되는 속성에 집중하세요. 또한 전체 하위 테이블을 복제하는 대신 집계를 증분 계산하기 위해 머티리얼라이즈드 뷰를 고려하세요. 스키마 업데이트가 드물고 지연 시간이 중요할 때 역정규화가 최고의 성능 트레이드오프를 제공합니다. ClickHouse에서 데이터 역정규화에 대한 전체 가이드는 여기를 참고하세요.

JOIN이 필요할 때

JOIN이 필요하다면 최소한 24.12 버전, 가급적 최신 버전을 사용하고 있는지 확인하세요. 각 릴리스마다 JOIN 성능이 계속 개선되기 때문입니다. ClickHouse 24.12부터 쿼리 플래너는 최적의 성능을 위해 이제 자동으로 더 작은 테이블을 join의 오른쪽에 배치합니다 — 이전에는 수동으로 해야 했던 작업이죠. 더 적극적인 필터 푸시다운과 여러 JOIN의 자동 재정렬을 포함한 더 많은 개선이 곧 제공될 예정입니다. JOIN 성능을 개선하려면 다음 모범 사례를 따르세요:

  • 데카르트 곱을 피하세요: 왼쪽의 값이 오른쪽의 여러 값과 일치하면 JOIN이 여러 행을 반환합니다 — 이른바 데카르트 곱이죠. 사용 사례가 오른쪽의 모든 일치가 아니라 단 하나의 일치만 필요하다면 ANY JOIN(예: LEFT ANY JOIN)을 사용할 수 있습니다. 일반 JOIN보다 빠르고 메모리를 덜 사용합니다.
  • JOIN되는 테이블의 크기를 줄이세요: JOIN의 실행 시간과 메모리 소비는 왼쪽과 오른쪽 테이블의 크기에 비례해 증가합니다. JOIN이 처리하는 데이터 양을 줄이려면 쿼리의 WHERE 또는 JOIN ON 절에 추가 필터 조건을 넣으세요. ClickHouse는 필터 조건을 쿼리 계획에서 가능한 깊게, 보통 JOIN 전에 밀어 넣습니다. 필터가 (어떤 이유로든) 자동으로 푸시다운되지 않으면 JOIN의 한쪽을 서브쿼리로 다시 작성해 푸시다운을 강제하세요.
  • 적절하다면 딕셔너리를 통한 직접(direct) JOIN을 사용하세요: ClickHouse의 표준 JOIN은 두 단계로 실행됩니다. 오른쪽을 반복하며 해시 테이블을 만드는 빌드(build) 단계와, 해시 테이블 조회로 일치하는 join 파트너를 찾기 위해 왼쪽을 반복하는 프로브(probe) 단계입니다. 오른쪽이 딕셔너리이거나 key-value 특성을 가진 다른 테이블 엔진(예: EmbeddedRocksDB 또는 Join 테이블 엔진)이라면 ClickHouse는 해시 테이블을 만들 필요를 사실상 없애는 "직접(direct)" join 알고리즘을 사용할 수 있어 쿼리 처리를 가속화합니다. 이것은 INNERLEFT OUTER JOIN에서 동작하며 실시간 분석 워크로드에 선호됩니다.
  • JOIN에 테이블 정렬을 활용하세요: ClickHouse의 각 테이블은 테이블의 프라이머리 키 컬럼으로 정렬됩니다. full_sorting_mergepartial_merge 같은 이른바 sort-merge JOIN 알고리즘을 사용해 테이블의 정렬을 활용할 수 있습니다. 해시 테이블 기반의 표준 JOIN 알고리즘(아래 parallel_hash, hash, grace_hash 참고)과 달리 sort-merge JOIN 알고리즘은 두 테이블을 먼저 정렬한 다음 병합합니다. 쿼리가 두 테이블을 각자의 프라이머리 키 컬럼으로 조인한다면 sort-merge는 정렬 단계를 생략하는 최적화가 있어 처리 시간과 오버헤드를 절약합니다.
  • 디스크 스필링(spilling) JOIN을 피하세요: JOIN의 중간 상태(예: 해시 테이블)가 너무 커져 주 메모리에 들어가지 못할 수 있습니다. 이 상황에서 ClickHouse는 기본적으로 메모리 부족 오류를 반환합니다. grace_hash, partial_merge, full_sorting_merge 같은 일부 join 알고리즘은 중간 상태를 디스크로 스필하고 쿼리 실행을 계속할 수 있습니다. 그러나 디스크 접근이 join 처리를 크게 느리게 할 수 있으므로 이러한 join 알고리즘은 주의해서 사용해야 합니다. 대신 중간 상태의 크기를 줄이는 다른 방식으로 JOIN 쿼리를 최적화하는 것을 권장합니다.
  • 외부 JOIN에서 무일치 표시자로 기본값 사용하기: left/right/full outer join은 왼쪽/오른쪽/양쪽 테이블의 모든 값을 포함합니다. 다른 테이블에서 어떤 값에 대한 join 파트너를 찾지 못하면 ClickHouse는 join 파트너를 특별한 표시자로 대체합니다. SQL 표준은 데이터베이스가 그러한 표시자로 NULL을 사용하도록 강제합니다. ClickHouse에서는 결과 컬럼을 Nullable로 감싸야 하므로 추가 메모리와 성능 오버헤드가 생깁니다. 대안으로 join_use_nulls = 0 설정을 구성하고 결과 컬럼 데이터 타입의 기본값을 표시자로 사용할 수 있습니다.

딕셔너리를 신중히 사용하세요 ClickHouse에서 JOIN에 딕셔너리를 사용할 때, 딕셔너리는 설계상 중복 키를 허용하지 않는다는 점을 이해하는 것이 중요합니다. 데이터 로딩 중에 중복 키는 조용히 중복 제거됩니다 — 주어진 키에 대해 마지막으로 로드된 값만 유지됩니다. 이 동작은 일대일 또는 다대일 관계에서 최신 또는 권위 있는 값만 필요한 경우에 딕셔너리를 이상적으로 만듭니다. 그러나 일대다 또는 다대다 관계(예: 배우가 여러 역할을 가질 수 있는 경우 roles를 actors에 조인)에서 딕셔너리를 사용하면 일치하는 행 중 하나만 남고 나머지는 모두 버려져 조용한 데이터 손실이 발생합니다. 결과적으로 딕셔너리는 여러 일치에 걸친 완전한 관계형 충실도가 필요한 시나리오에는 적합하지 않습니다. 딕셔너리가 언제 도움이 되는지(그리고 언제 안 되는지)에 대한 자세한 내용은 딕셔너리 모범 사례를 참고하세요.

올바른 JOIN 알고리즘 선택하기

ClickHouse는 속도와 메모리 사이에서 트레이드오프를 제공하는 여러 JOIN 알고리즘을 지원합니다:

  • Parallel Hash JOIN (기본): 메모리에 들어가는 소형~중형 오른쪽 테이블에 빠릅니다.
  • Direct JOIN: INNER 또는 LEFT ANY JOIN과 함께 딕셔너리(또는 key-value 특성을 가진 다른 테이블 엔진)를 사용할 때 이상적입니다 — 해시 테이블을 만들 필요를 없애 점 조회(point lookups)에 가장 빠른 방법입니다.
  • Full Sorting Merge JOIN: 두 테이블이 조인 키로 정렬되어 있을 때 효율적입니다.
  • Partial Merge JOIN: 메모리를 최소화하지만 더 느립니다 — 제한된 메모리로 큰 테이블을 조인할 때 가장 좋습니다.
  • Grace Hash JOIN: 유연하고 메모리 조정이 가능하며 조정 가능한 성능 특성으로 큰 데이터셋에 좋습니다.

각 알고리즘은 JOIN 타입에 대한 지원이 다양합니다. 각 알고리즘에 대해 지원되는 조인 타입의 전체 목록은 여기에서 찾을 수 있습니다.

join_algorithm = 'auto'를 설정해 ClickHouse가 최상의 알고리즘을 선택하게 하거나, 워크로드에 따라 명시적으로 제어할 수 있습니다. 기본값은 direct,parallel_hash,hash이므로 ClickHouse는 오른쪽이 딕셔너리나 key-value 엔진일 때 직접(direct) join을 사용하고, 그렇지 않으면 parallel hash, 이어서 hash로 폴백합니다. 성능이나 메모리 오버헤드를 최적화하기 위해 join 알고리즘을 선택해야 한다면 이 가이드를 추천합니다. 최적의 성능을 위해:

  • 고성능 워크로드에서 JOIN을 최소화하세요.
  • 쿼리당 3~4개 이상의 join은 피하세요.
  • 실제 데이터로 다른 알고리즘을 벤치마킹하세요 — 성능은 JOIN 키 분포와 데이터 크기에 따라 다릅니다.

JOIN 최적화 전략, JOIN 알고리즘, 튜닝 방법에 대한 자세한 내용은 ClickHouse 문서와 이 블로그 시리즈를 참고하세요.

더 알아보기 (Learn more)