메인 콘텐츠로 건너뛰기
ClickHouse는 다양한 조인 알고리즘과 함께 포괄적인 JOIN 지원을 제공합니다. 성능을 극대화하려면 이 가이드에 나열된 조인 최적화 권장 사항을 따르는 것이 좋습니다.
  • 최적의 성능을 위해서는 쿼리의 JOIN 수를 줄이는 것이 좋으며, 특히 밀리초 단위의 응답 성능이 필요한 실시간 분석 워크로드에서는 더욱 중요합니다. 하나의 쿼리에서 조인은 최대 3~4개를 목표로 하십시오. 데이터 모델링 섹션에서는 비정규화, Dictionaries, 구체화된 뷰(Materialized View)를 포함해 조인을 최소화하는 여러 방법을 자세히 설명합니다.
  • ClickHouse 24.12부터는 쿼리 플래너가 최적의 성능을 위해 2개 테이블 조인의 순서를 자동으로 재정렬하여 더 작은 테이블을 오른쪽에 배치합니다. 25.9 버전에서는 이 기능이 확장되어 3개 이상의 테이블을 조인하는 쿼리의 조인 순서까지 최적화할 수 있게 되었습니다.
  • 쿼리에 Direct JOIN, 즉 아래와 같은 LEFT ANY JOIN이 필요한 경우에는 가능하면 Dictionaries를 사용하는 것이 좋습니다.
  • inner join을 수행하는 경우에는 IN 절을 사용하는 서브쿼리로 작성하는 편이 더 효율적인 경우가 많습니다. 다음 쿼리를 살펴보십시오. 두 쿼리는 기능적으로 동일합니다. 둘 다 질문에는 ClickHouse가 언급되지 않았지만 comments에는 언급된 posts의 수를 찾습니다.
SELECT count()
FROM stackoverflow.posts AS p
ANY INNER `JOIN` stackoverflow.comments AS c ON p.Id = c.PostId
WHERE (p.Title != '') AND (p.Title NOT ILIKE '%clickhouse%') AND (p.Body NOT ILIKE '%clickhouse%') AND (c.Text ILIKE '%clickhouse%')

┌─count()─┐
86
└─────────┘

1 row in set. Elapsed: 8.209 sec. Processed 150.20 million rows, 56.05 GB (18.30 million rows/s., 6.83 GB/s.)
Peak memory usage: 1.23 GiB.
INNER join만이 아니라 ANY INNER JOIN을 사용하는 이유는 카테시안 곱이 발생하지 않도록 하기 위해서입니다. 즉, 각 게시물마다 일치하는 항목은 하나만 있으면 됩니다. 이 조인(join)은 서브쿼리(subquery)를 사용해 다시 작성할 수 있으며, 이렇게 하면 성능이 크게 향상됩니다:
SELECT count()
FROM stackoverflow.posts
WHERE (Title != '') AND (Title NOT ILIKE '%clickhouse%') AND (Body NOT ILIKE '%clickhouse%') AND (Id IN (
        SELECT PostId
        FROM stackoverflow.comments
        WHERE Text ILIKE '%clickhouse%'
))
┌─count()─┐
86
└─────────┘

1 row in set. Elapsed: 2.284 sec. Processed 150.20 million rows, 16.61 GB (65.76 million rows/s., 7.27 GB/s.)
Peak memory usage: 323.52 MiB.
ClickHouse는 모든 JOIN 절과 서브쿼리에 조건을 푸시다운하려고 시도하지만, 가능하다면 항상 모든 하위 절에도 조건을 수동으로 적용할 것을 권장합니다. 이렇게 하면 JOIN할 데이터의 크기를 최소화할 수 있습니다. 아래 예시에서는 2020년 이후 Java 관련 게시물의 up-vote 수를 계산하려고 합니다. 더 큰 테이블을 왼쪽에 둔 단순한 쿼리는 56초 만에 완료됩니다:
SELECT countIf(VoteTypeId = 2) AS upvotes
FROM stackoverflow.posts AS p
INNER JOIN stackoverflow.votes AS v ON p.Id = v.PostId
WHERE has(arrayFilter(t -> (t != ''), splitByChar('|', p.Tags)), 'java') AND (p.CreationDate >= '2020-01-01')

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 56.642 sec. Processed 252.30 million rows, 1.62 GB (4.45 million rows/s., 28.60 MB/s.)
이 join의 순서를 바꾸면 성능이 크게 개선되어 실행 시간이 1.5초로 줄어듭니다:
SELECT countIf(VoteTypeId = 2) AS upvotes
FROM stackoverflow.votes AS v
INNER JOIN stackoverflow.posts AS p ON v.PostId = p.Id
WHERE has(arrayFilter(t -> (t != ''), splitByChar('|', p.Tags)), 'java') AND (p.CreationDate >= '2020-01-01')

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 1.519 sec. Processed 252.30 million rows, 1.62 GB (166.06 million rows/s., 1.07 GB/s.)
왼쪽 테이블에 필터를 추가하면 성능이 더욱 향상되어 실행 시간이 0.5초까지 단축됩니다.
SELECT countIf(VoteTypeId = 2) AS upvotes
FROM stackoverflow.votes AS v
INNER JOIN stackoverflow.posts AS p ON v.PostId = p.Id
WHERE has(arrayFilter(t -> (t != ''), splitByChar('|', p.Tags)), 'java') AND (p.CreationDate >= '2020-01-01') AND (v.CreationDate >= '2020-01-01')

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 0.597 sec. Processed 81.14 million rows, 1.31 GB (135.82 million rows/s., 2.19 GB/s.)
Peak memory usage: 249.42 MiB.
앞서 언급한 것처럼, INNER JOIN을 서브쿼리로 옮기고 외부 쿼리와 내부 쿼리 모두에 필터를 유지하면 이 쿼리를 한층 더 개선할 수 있습니다.
SELECT count() AS upvotes
FROM stackoverflow.votes
WHERE (VoteTypeId = 2) AND (PostId IN (
        SELECT Id
        FROM stackoverflow.posts
        WHERE (CreationDate >= '2020-01-01') AND has(arrayFilter(t -> (t != ''), splitByChar('|', Tags)), 'java')
))

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 0.383 sec. Processed 99.64 million rows, 804.55 MB (259.85 million rows/s., 2.10 GB/s.)
Peak memory usage: 250.66 MiB.

JOIN 알고리즘 선택

ClickHouse는 여러 조인 알고리즘을 지원합니다. 이러한 알고리즘은 일반적으로 메모리 사용량과 성능을 맞바꾸는 특성이 있습니다. 아래에서는 상대적인 메모리 사용량과 실행 시간을 기준으로 ClickHouse 조인 알고리즘을 개괄적으로 설명합니다.

이러한 알고리즘은 조인 쿼리가 계획되고 실행되는 방식을 결정합니다. 기본적으로 ClickHouse는 사용된 조인 유형, 엄격성, 그리고 조인되는 테이블의 engine에 따라 direct 또는 해시 조인 알고리즘을 사용합니다. 또한 리소스 가용성과 사용량에 따라 런타임에 사용할 조인 알고리즘을 적응적으로 선택하고 동적으로 변경하도록 ClickHouse를 구성할 수 있습니다. join_algorithm=auto인 경우 ClickHouse는 먼저 해시 조인 알고리즘을 시도하고, 해당 알고리즘의 메모리 제한을 초과하면 실행 중에 부분 병합 조인으로 전환합니다. 어떤 알고리즘이 선택되었는지는 trace 로깅을 통해 확인할 수 있습니다. 또한 ClickHouse는 join_algorithm setting을 통해 원하는 조인 알고리즘을 직접 지정할 수 있습니다. 각 조인 알고리즘에서 지원하는 JOIN 유형은 아래와 같으며, 최적화 전에 이를 고려해야 합니다.

JOIN 알고리즘에 대한 자세한 설명은 여기에서 확인할 수 있으며, 각 알고리즘의 장점, 단점, 스케일링 특성도 함께 설명합니다. 적절한 조인 알고리즘을 선택할 때는 메모리와 성능 중 무엇을 우선적으로 최적화할지에 따라 달라집니다.

JOIN 성능 최적화

핵심 최적화 지표가 성능이고 조인을 가능한 한 빠르게 실행하려는 경우, 적절한 조인 알고리즘을 선택하는 데 다음 의사결정 트리를 사용할 수 있습니다.

  • (1) 오른쪽 테이블의 데이터를 딕셔너리와 같은 인메모리 저지연 키-값 데이터 구조에 미리 로드할 수 있고, 조인 키가 기반 키-값 저장소의 키 속성과 일치하며, LEFT ANY JOIN 동작으로 충분하다면 Direct JOIN을 적용할 수 있으며 가장 빠른 방식입니다.
  • (2) 테이블의 물리적 행 순서가 조인 키의 정렬 순서와 일치한다면, 상황에 따라 달라집니다. 이 경우 full sorting merge join은 정렬 단계를 건너뛰므로 메모리 사용량이 크게 줄어들고, 데이터 크기와 조인 키 값의 분포에 따라 일부 해시 조인 알고리즘보다 더 빠르게 실행될 수도 있습니다.
  • (3) 오른쪽 테이블이 parallel hash join추가 메모리 사용 오버헤드를 감안하더라도 메모리에 들어간다면, 이 알고리즘 또는 해시 조인이 더 빠를 수 있습니다. 이는 데이터 크기, 데이터 타입, 조인 키 컬럼의 값 분포에 따라 달라집니다.
  • (4) 오른쪽 테이블이 메모리에 들어가지 않는다면, 이 경우도 상황에 따라 달라집니다. ClickHouse는 메모리 제약을 받지 않는 조인 알고리즘을 세 가지 제공합니다. 이 세 가지는 모두 데이터를 일시적으로 디스크에 spill합니다. Full sorting merge join부분 병합 조인은 먼저 데이터를 정렬해야 합니다. 반면 Grace hash join은 대신 데이터로 해시 테이블을 구축합니다. 데이터 양, 데이터 타입, 조인 키 컬럼의 값 분포에 따라 데이터를 정렬하는 것보다 해시 테이블을 구축하는 편이 더 빠를 수 있습니다. 반대의 경우도 마찬가지입니다.
부분 병합 조인은 큰 테이블을 조인할 때 메모리 사용량을 최소화하도록 최적화되어 있지만, 그 대가로 조인 속도는 상당히 느립니다. 특히 왼쪽 테이블의 물리적 행 순서가 조인 키 정렬 순서와 일치하지 않을 때 더욱 그렇습니다. Grace hash join은 세 가지 메모리 비제약 조인 알고리즘 중 가장 유연하며, grace_hash_join_initial_buckets 설정을 통해 메모리 사용량과 조인 속도 간의 균형을 효과적으로 제어할 수 있습니다. 데이터 양에 따라, 두 알고리즘의 메모리 사용량이 대체로 비슷해지도록 버킷 수를 선택하면 grace hash가 부분 병합 조인 알고리즘보다 더 빠를 수도 있고 더 느릴 수도 있습니다. grace hash join의 메모리 사용량을 full sorting merge의 메모리 사용량과 대체로 비슷하게 맞추면, 테스트에서는 full sorting merge가 항상 더 빨랐습니다. 세 가지 메모리 비제약 알고리즘 중 어느 것이 가장 빠른지는 데이터 양, 데이터 타입, 조인 키 컬럼의 값 분포에 따라 달라집니다. 어떤 알고리즘이 가장 빠른지 판단하려면 실제와 유사한 데이터와 데이터 규모로 벤치마크를 수행하는 것이 가장 좋습니다.

메모리 사용량 최적화

가장 빠른 실행 시간보다 메모리 사용량을 최소화하도록 조인을 최적화하려면, 다음 의사결정 트리를 사용할 수 있습니다:

  • (1) 테이블의 물리적 행 순서가 조인 키의 정렬 순서와 일치하면 full sorting merge join의 메모리 사용량은 최저 수준까지 낮아집니다. 또한 정렬 단계가 비활성화되므로 조인 속도도 우수하다는 장점이 있습니다.
  • (2) grace hash join은 조인 속도를 어느 정도 희생하는 대신, 많은 수의 버킷을 사용하도록 구성하여 메모리 사용량을 매우 낮게 조정할 수 있습니다. 부분 병합 조인은 의도적으로 적은 양의 주 메모리를 사용합니다. 외부 정렬이 활성화된 full sorting merge join은 일반적으로 부분 병합 조인보다 더 많은 메모리를 사용하지만(행 순서가 키 정렬 순서와 일치하지 않는다고 가정), 그 대신 조인 실행 시간은 훨씬 더 우수합니다.
위 내용을 더 자세히 확인하려면 다음 블로그 시리즈를 참고하십시오.
마지막 수정일 2026년 6월 10일