메인 콘텐츠로 건너뛰기
JOIN 절은 각 테이블에 공통으로 존재하는 값을 사용해 하나 이상의 테이블에서 컬럼을 결합하고, 그 결과로 새 테이블을 만듭니다. 이는 SQL을 지원하는 데이터베이스에서 흔히 사용되는 연산이며, 관계대수의 조인에 해당합니다. 하나의 테이블을 자기 자신과 조인하는 특수한 경우는 보통 “self-join”이라고 합니다. 구문
SELECT <expr_list>
FROM <left_table>
[GLOBAL] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER|SEMI|ANTI|ANY|ALL|ASOF] JOIN <right_table>
(ON <expr_list>)|(USING <column_list>) ...
ON 절의 표현식과 USING 절의 컬럼을 “조인 키”라고 합니다. 별도로 명시하지 않는 한, JOIN은 “조인 키”가 일치하는 행들로 카테시안 곱을 생성하므로, 원본 테이블보다 훨씬 많은 행이 결과에 포함될 수 있습니다.

지원되는 JOIN 유형

모든 표준 SQL JOIN 유형을 지원합니다:
유형설명
INNER JOIN일치하는 행만 반환됩니다.
LEFT OUTER JOIN일치하는 행과 함께 왼쪽 테이블의 비일치 행도 반환됩니다.
RIGHT OUTER JOIN일치하는 행과 함께 오른쪽 테이블의 비일치 행도 반환됩니다.
FULL OUTER JOIN일치하는 행과 함께 양쪽 테이블의 비일치 행도 반환됩니다.
CROSS JOIN전체 테이블의 카테시안 곱을 생성하며, “조인 키”는 지정하지 않습니다.
NATURAL JOIN두 테이블에서 이름이 같은 모든 컬럼을 기준으로 자동으로 조인하며, 각 공통 컬럼은 결과에 한 번만 나타납니다. INNER(기본값), LEFT, RIGHT, FULL 변형을 지원합니다. 컬럼 목록이 자동으로 도출된다는 점만 제외하면 JOIN ... USING (col1, col2, ...)와 동일합니다.
  • 유형을 지정하지 않은 JOININNER를 의미합니다.
  • OUTER 키워드는 생략해도 됩니다.
  • CROSS JOIN의 대체 구문으로, FROM에서 여러 테이블을 쉼표로 구분해 지정할 수 있습니다.
  • NATURAL JOIN에 일치하는 컬럼이 없으면 CROSS JOIN처럼 동작합니다.
ClickHouse에서 추가로 지원하는 조인 유형은 다음과 같습니다:
유형설명
LEFT SEMI JOIN, RIGHT SEMI JOIN카테시안 곱을 생성하지 않고 “조인 키”에 대해 허용 목록을 적용합니다.
LEFT ANTI JOIN, RIGHT ANTI JOIN카테시안 곱을 생성하지 않고 “조인 키”에 대해 거부 목록을 적용합니다.
LEFT ANY JOIN, RIGHT ANY JOIN, INNER ANY JOIN표준 JOIN 유형의 카테시안 곱을 부분적으로(LEFTRIGHT의 반대편) 또는 완전히(INNERFULL) 비활성화합니다.
ASOF JOIN, LEFT ASOF JOIN정확히 일치하지 않는 조건으로 시퀀스를 조인합니다. ASOF JOIN 사용법은 아래에 설명되어 있습니다.
PASTE JOIN두 테이블을 가로로 이어 붙입니다.
join_algorithmpartial_merge로 설정된 경우, RIGHT JOINFULL JOINALL 엄격성에서만 지원됩니다(SEMI, ANTI, ANY, ASOF는 지원되지 않음).

설정

기본 조인 유형은 join_default_strictness 설정으로 재정의할 수 있습니다. ANY JOIN 작업에서 ClickHouse 서버의 동작은 any_join_distinct_right_table_keys 설정에 따라 달라집니다. 관련 항목 ClickHouse가 CROSS JOININNER JOIN으로 재작성하지 못할 때의 동작은 cross_to_inner_join_rewrite 설정으로 지정할 수 있습니다. 기본값은 1이며, 이 경우 조인은 계속 수행되지만 더 느려집니다. 오류를 발생시키려면 cross_to_inner_join_rewrite0으로 설정하고, 크로스 조인을 실행하지 않고 대신 모든 쉼표 조인/크로스 조인을 재작성하도록 강제하려면 2로 설정하십시오. 값이 2일 때 재작성에 실패하면 “Please, try to simplify WHERE section”라는 오류 메시지가 표시됩니다.

ON 절의 조건

ON 절에는 ANDOR 연산자로 결합된 여러 조건을 포함할 수 있습니다. 조인 키를 지정하는 조건은 다음 요구 사항을 충족해야 합니다.
  • 왼쪽 테이블과 오른쪽 테이블을 모두 참조해야 합니다
  • 동등 연산자를 사용해야 합니다
그 밖의 조건에서는 다른 논리 연산자를 사용할 수 있지만, 쿼리의 왼쪽 테이블 또는 오른쪽 테이블 중 하나만 참조해야 합니다. 전체 복합 조건이 충족되면 행이 조인됩니다. 조건이 충족되지 않더라도 JOIN 유형에 따라 행이 결과에 포함될 수 있습니다. 같은 조건을 WHERE 절에 넣었는데 조건이 충족되지 않으면, 행은 항상 결과에서 필터링된다는 점에 유의하십시오. ON 절 내부의 OR 연산자는 해시 조인 알고리즘을 사용해 동작합니다. 즉, JOIN의 조인 키가 있는 각 OR 인수마다 별도의 해시 테이블이 생성되므로, ON 절의 OR 표현식 수가 늘어날수록 메모리 활용량과 쿼리 실행 시간은 선형적으로 증가합니다.
조건이 서로 다른 테이블의 컬럼을 참조하는 경우 현재는 동등 연산자(=)만 지원됩니다.
예시 table_1table_2를 가정해 보겠습니다.
┌─Id─┬─name─┐     ┌─Id─┬─text───────────┬─scores─┐
│  1 │ A    │     │  1 │ Text A         │     10 │
│  2 │ B    │     │  1 │ Another text A │     12 │
│  3 │ C    │     │  2 │ Text B         │     15 │
└────┴──────┘     └────┴────────────────┴────────┘
하나의 JOIN 키 조건과 table_2에 대한 추가 조건이 포함된 쿼리:
Query
SELECT name, text FROM table_1 LEFT OUTER JOIN table_2
    ON table_1.Id = table_2.Id AND startsWith(table_2.text, 'Text');
결과에는 이름이 C이고 text 컬럼이 비어 있는 행도 포함된다는 점에 유의하십시오. 이는 OUTER join을 사용했기 때문에 결과에 포함된 것입니다.
Response
┌─name─┬─text───┐
│ A    │ Text A │
│ B    │ Text B │
│ C    │        │
└──────┴────────┘
INNER 유형의 조인에서 여러 조건을 사용하는 쿼리:
Query
SELECT name, text, scores FROM table_1 INNER JOIN table_2
    ON table_1.Id = table_2.Id AND table_2.scores > 10 AND startsWith(table_2.text, 'Text');
Response
┌─name─┬─text───┬─scores─┐
│ B    │ Text B │     15
└──────┴────────┴────────┘
조인의 INNER 유형과 OR 조건을 사용하는 쿼리:
Query
CREATE TABLE t1 (`a` Int64, `b` Int64) ENGINE = MergeTree() ORDER BY a;

CREATE TABLE t2 (`key` Int32, `val` Int64) ENGINE = MergeTree() ORDER BY key;

INSERT INTO t1 SELECT number as a, -a as b from numbers(5);

INSERT INTO t2 SELECT if(number % 2 == 0, toInt64(number), -number) as key, number as val from numbers(5);

SELECT a, b, val FROM t1 INNER JOIN t2 ON t1.a = t2.key OR t1.b = t2.key;
Response
┌─a─┬──b─┬─val─┐
│ 0 │  0 │   0 │
│ 1 │ -1 │   1 │
│ 2 │ -2 │   2 │
│ 3 │ -3 │   3 │
│ 4 │ -4 │   4 │
└───┴────┴─────┘
조인 유형이 INNER이며 조건에 ORAND를 사용하는 쿼리:
기본적으로 같지 않은 조건은 같은 테이블의 컬럼을 사용하는 경우에만 지원됩니다. 예를 들어 t1.a = t2.key AND t1.b > 0 AND t2.b > t2.ct1.b > 0t1의 컬럼만 사용하고 t2.b > t2.ct2의 컬럼만 사용하므로 지원됩니다. 하지만 t1.a = t2.key AND t1.b > t2.key와 같은 조건에 대해서는 실험적 지원을 사용해 볼 수 있습니다. 자세한 내용은 아래 섹션을 확인하십시오.
Query
SELECT a, b, val FROM t1 INNER JOIN t2 ON t1.a = t2.key OR t1.b = t2.key AND t2.val > 3;
Response
┌─a─┬──b─┬─val─┐
│ 0 │  0 │   0 │
│ 2 │ -2 │   2 │
│ 4 │ -4 │   4 │
└───┴────┴─────┘

서로 다른 테이블의 컬럼에 대한 부등 조건 JOIN

ClickHouse는 현재 동등 조건뿐 아니라 부등 조건도 포함한 ALL/ANY/SEMI/ANTI INNER/LEFT/RIGHT/FULL JOIN을 지원합니다. 부등 조건은 hashgrace_hash 조인 알고리즘에서만 지원됩니다. 부등 조건은 join_use_nulls와 함께는 지원되지 않습니다. 예시 테이블 t1:
┌─key──┬─attr─┬─a─┬─b─┬─c─┐
│ key1 │ a    │ 1 │ 1 │ 2 │
│ key1 │ b    │ 2 │ 3 │ 2 │
│ key1 │ c    │ 3 │ 2 │ 1 │
│ key1 │ d    │ 4 │ 7 │ 2 │
│ key1 │ e    │ 5 │ 5 │ 5 │
│ key2 │ a2   │ 1 │ 1 │ 1 │
│ key4 │ f    │ 2 │ 3 │ 4 │
└──────┴──────┴───┴───┴───┘
테이블 t2
┌─key──┬─attr─┬─a─┬─b─┬─c─┐
│ key1 │ A    │ 1 │ 2 │ 1 │
│ key1 │ B    │ 2 │ 1 │ 2 │
│ key1 │ C    │ 3 │ 4 │ 5 │
│ key1 │ D    │ 4 │ 1 │ 6 │
│ key3 │ a3   │ 1 │ 1 │ 1 │
│ key4 │ F    │ 1 │ 1 │ 1 │
└──────┴──────┴───┴───┴───┘
SELECT t1.*, t2.* FROM t1 LEFT JOIN t2 ON t1.key = t2.key AND (t1.a < t2.a) ORDER BY (t1.key, t1.attr, t2.key, t2.attr);
key1    a    1    1    2    key1    B    2    1    2
key1    a    1    1    2    key1    C    3    4    5
key1    a    1    1    2    key1    D    4    1    6
key1    b    2    3    2    key1    C    3    4    5
key1    b    2    3    2    key1    D    4    1    6
key1    c    3    2    1    key1    D    4    1    6
key1    d    4    7    2            0    0    \N
key1    e    5    5    5            0    0    \N
key2    a2    1    1    1            0    0    \N
key4    f    2    3    4            0    0    \N

JOIN 키의 NULL 값

NULL은 자기 자신을 포함해 어떤 값과도 같지 않습니다. 즉, 한 테이블에서 JOIN 키의 값이 NULL이면 다른 테이블의 NULL 값과 일치하지 않습니다. 예시 테이블 A:
┌───id─┬─name────┐
│    1 │ Alice   │
│    2 │ Bob     │
│ ᴺᵁᴸᴸ │ Charlie │
└──────┴─────────┘
테이블 B:
┌───id─┬─score─┐
│    1 │    90 │
│    3 │    85 │
│ ᴺᵁᴸᴸ │    88 │
└──────┴───────┘
SELECT A.name, B.score FROM A LEFT JOIN B ON A.id = B.id
┌─name────┬─score─┐
│ Alice   │    90 │
│ Bob     │     0 │
│ Charlie │     0 │
└─────────┴───────┘
A 테이블의 Charlie가 있는 행과 B 테이블의 점수 88인 행은 JOIN 키에 NULL 값이 있기 때문에 결과에 포함되지 않습니다. NULL 값을 서로 일치시키려면 JOIN 키를 비교할 때 isNotDistinctFrom 함수를 사용하십시오.
SELECT A.name, B.score FROM A LEFT JOIN B ON isNotDistinctFrom(A.id, B.id)
┌─name────┬─score─┐
│ Alice   │    90 │
│ Bob     │     0 │
│ Charlie │    88 │
└─────────┴───────┘

ASOF JOIN 사용

정확히 일치하는 항목이 없는 레코드를 조인해야 할 때 ASOF JOIN이 유용합니다. 이 JOIN 알고리즘을 사용하려면 테이블에 특수한 컬럼이 필요합니다. 이 컬럼은 다음 조건을 충족해야 합니다.
  • 정렬된 시퀀스를 포함해야 합니다.
  • 다음 타입 중 하나여야 합니다: Int, UInt, Float, Date, DateTime, Decimal.
  • hash 조인 알고리즘에서는 JOIN 절의 유일한 컬럼일 수 없습니다.
구문 ASOF JOIN ... ON:
SELECT expressions_list
FROM table_1
ASOF LEFT JOIN table_2
ON equi_cond AND closest_match_cond
등가 조건은 개수 제한 없이 사용할 수 있으며, 가장 가까운 일치 조건은 정확히 하나만 사용할 수 있습니다. 예를 들어, SELECT count() FROM table_1 ASOF LEFT JOIN table_2 ON table_1.a == table_2.b AND table_2.t <= table_1.t. 가장 가까운 일치에 지원되는 조건: >, >=, <, <=. 구문 ASOF JOIN ... USING:
SELECT expressions_list
FROM table_1
ASOF JOIN table_2
USING (equi_column1, ... equi_columnN, asof_column)
ASOF JOIN은 동등 조건으로 조인할 때 equi_columnX를 사용하고, table_1.asof_column >= table_2.asof_column 조건에서 가장 가까운 값을 기준으로 조인할 때는 asof_column을 사용합니다. asof_column 컬럼은 항상 USING 절의 마지막에 위치합니다. 예시로, 다음 테이블을 살펴보겠습니다.
         table_1                           table_2
      event   | ev_time | user_id       event   | ev_time | user_id
    ----------|---------|----------   ----------|---------|----------
                  ...                               ...
    event_1_1 |  12:00  |  42         event_2_1 |  11:59  |   42
                  ...                 event_2_2 |  12:30  |   42
    event_1_2 |  13:00  |  42         event_2_3 |  13:00  |   42
                  ...                               ...
ASOF JOINtable_1의 사용자 이벤트 타임스탬프를 기준으로, 가장 가까운 일치 조건에 맞으면서 table_1의 이벤트 타임스탬프와 가장 가까운 타임스탬프를 가진 table_2의 이벤트를 찾을 수 있습니다. 같은 타임스탬프 값이 있으면 그것이 가장 가까운 값으로 선택됩니다. 여기서 user_id 컬럼은 동등 조건 조인에 사용할 수 있고, ev_time 컬럼은 최근접 일치 조인에 사용할 수 있습니다. 이 예시에서는 event_1_1event_2_1과 조인할 수 있고 event_1_2event_2_3과 조인할 수 있지만, event_2_2는 조인할 수 없습니다.
ASOF JOINhashfull_sorting_merge 조인 알고리즘에서만 지원됩니다. Join 테이블 엔진에서는 지원되지 않습니다.

PASTE JOIN 사용법

PASTE JOIN의 결과는 왼쪽 서브쿼리의 모든 컬럼 뒤에 오른쪽 서브쿼리의 모든 컬럼이 이어지는 테이블입니다. 행은 원본 테이블에서의 위치를 기준으로 대응됩니다(행 순서가 정의되어 있어야 합니다). 서브쿼리가 반환하는 행 수가 서로 다르면 초과된 행은 잘립니다. 예시:
SELECT *
FROM
(
    SELECT number AS a
    FROM numbers(2)
) AS t1
PASTE JOIN
(
    SELECT number AS a
    FROM numbers(2)
    ORDER BY a DESC
) AS t2

┌─a─┬─t2.a─┐
01
10
└───┴──────┘
참고: 이 경우 병렬로 읽으면 결과가 비결정적일 수 있습니다. 예시는 다음과 같습니다:
SELECT *
FROM
(
    SELECT number AS a
    FROM numbers_mt(5)
) AS t1
PASTE JOIN
(
    SELECT number AS a
    FROM numbers(10)
    ORDER BY a DESC
) AS t2
SETTINGS max_block_size = 2;

┌─a─┬─t2.a─┐
29
38
└───┴──────┘
┌─a─┬─t2.a─┐
07
16
└───┴──────┘
┌─a─┬─t2.a─┐
45
└───┴──────┘

분산 JOIN

분산 테이블이 포함된 JOIN을 실행하는 방법은 두 가지입니다.
  • 일반 JOIN을 사용하면 쿼리가 원격 서버로 전송됩니다. 오른쪽 테이블을 만들기 위해 각 서버에서 서브쿼리가 실행되며, 이렇게 만들어진 테이블로 조인이 수행됩니다. 즉, 오른쪽 테이블은 각 서버에서 각각 구성됩니다.
  • GLOBAL ... JOIN을 사용하면 먼저 요청 서버가 서브쿼리를 실행해 조인의 한쪽을 계산하고, 그 결과를 임시 테이블에 수집합니다. 그런 다음 이 임시 테이블이 각 원격 서버로 전달되고, 전송된 임시 데이터를 사용해 각 서버에서 쿼리가 실행됩니다. LEFTINNER 조인의 경우 오른쪽 테이블이 서브쿼리로 계산됩니다. RIGHT 조인의 경우 유지되는 쪽은 오른쪽 테이블이며 세그먼트에서 읽어야 하므로, 대신 왼쪽 테이블이 계산됩니다.
GLOBAL을 사용할 때는 주의하십시오. 자세한 내용은 분산 서브쿼리 섹션을 참조하십시오.

암시적 타입 변환

INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN 쿼리는 “조인 키”에 대한 암시적 타입 변환을 지원합니다. 하지만 왼쪽 테이블과 오른쪽 테이블의 join 키를 하나의 타입으로 변환할 수 없으면 쿼리를 실행할 수 없습니다(예를 들어 UInt64Int64의 모든 값을 모두 표현할 수 있는 데이터 타입이 없거나, StringInt32를 함께 표현할 수 있는 데이터 타입이 없는 경우). 예시 테이블 t_1을 살펴보겠습니다:
┌─a─┬─b─┬─toTypeName(a)─┬─toTypeName(b)─┐
│ 1 │ 1 │ UInt16        │ UInt8         │
│ 2 │ 2 │ UInt16        │ UInt8         │
└───┴───┴───────────────┴───────────────┘
그리고 t_2 테이블:
┌──a─┬────b─┬─toTypeName(a)─┬─toTypeName(b)───┐
│ -1 │    1 │ Int16         │ Nullable(Int64) │
│  1 │   -1 │ Int16         │ Nullable(Int64) │
│  1 │    1 │ Int16         │ Nullable(Int64) │
└────┴──────┴───────────────┴─────────────────┘
쿼리
SELECT a, b, toTypeName(a), toTypeName(b) FROM t_1 FULL JOIN t_2 USING (a, b);
다음 집합을 반환합니다:
┌──a─┬────b─┬─toTypeName(a)─┬─toTypeName(b)───┐
│  1 │    1 │ Int32         │ Nullable(Int64) │
│  2 │    2 │ Int32         │ Nullable(Int64) │
│ -1 │    1 │ Int32         │ Nullable(Int64) │
│  1 │   -1 │ Int32         │ Nullable(Int64) │
└────┴──────┴───────────────┴─────────────────┘

사용 권장 사항

비어 있거나 NULL인 셀 처리

테이블을 조인하는 동안 빈 셀이 나타날 수 있습니다. join_use_nulls 설정은 ClickHouse가 이러한 셀을 어떻게 채울지 결정합니다. JOIN 키가 널 허용 필드인 경우, 키 중 하나 이상이 NULL 값인 행은 조인되지 않습니다.

구문

USING에 지정된 컬럼은 두 서브쿼리에서 이름이 같아야 하며, 나머지 컬럼은 서로 다른 이름이어야 합니다. 별칭을 사용해 서브쿼리의 컬럼 이름을 변경할 수 있습니다. USING 절은 조인할 하나 이상의 컬럼을 지정하며, 이를 통해 해당 컬럼들이 서로 같은 값으로 매칭됩니다. 컬럼 목록은 괄호 없이 지정합니다. 더 복잡한 조인 조건은 지원되지 않습니다.

구문 제한 사항

단일 SELECT 쿼리에서 여러 JOIN 절을 사용하는 경우:
  • *로 모든 컬럼을 가져오는 것은 테이블을 조인한 경우에만 가능하며, 서브쿼리에는 사용할 수 없습니다.
  • PREWHERE 절은 사용할 수 없습니다.
  • USING 절은 사용할 수 없습니다.
ON, WHERE, GROUP BY 절:
  • ON, WHERE, GROUP BY 절에서는 임의의 표현식을 사용할 수 없지만, SELECT 절에서 표현식을 정의한 뒤 별칭을 통해 해당 절에서 사용할 수 있습니다.

성능

JOIN을 실행할 때는 쿼리의 다른 단계와의 관계에서 실행 순서가 최적화되지 않습니다. join(오른쪽 테이블을 검색하는 작업)은 WHERE 필터링보다 먼저, 그리고 집계보다 먼저 실행됩니다. 동일한 JOIN으로 쿼리를 실행할 때마다 결과가 캐시되지 않으므로 서브쿼리가 매번 다시 실행됩니다. 이를 방지하려면 항상 RAM에 상주하는, 조인용으로 미리 준비된 배열인 특수한 Join 테이블 엔진을 사용하십시오. 경우에 따라 JOIN 대신 IN을 사용하는 편이 더 효율적입니다. 차원 테이블(예: 광고 캠페인 이름과 같은 차원 속성을 담고 있는 비교적 작은 테이블)과 조인하기 위해 JOIN이 필요한 경우, 모든 쿼리마다 오른쪽 테이블에 다시 접근해야 하므로 JOIN이 그리 편리하지 않을 수 있습니다. 이런 경우에는 JOIN 대신 “딕셔너리” 기능을 사용하는 것이 좋습니다. 자세한 내용은 Dictionaries 섹션을 참조하십시오.

메모리 제한

기본적으로 ClickHouse는 해시 조인 알고리즘을 사용합니다. ClickHouse는 right_table을 가져와 이를 위한 해시 테이블을 RAM에 생성합니다. join_algorithm = 'auto'가 활성화되어 있으면, 메모리 사용량이 일정 임계값에 도달한 후 ClickHouse는 머지 조인 알고리즘으로 전환합니다. JOIN 알고리즘에 대한 설명은 join_algorithm 설정을 참조하십시오. JOIN 연산의 메모리 사용량을 제한해야 하는 경우 다음 설정을 사용하십시오. 이러한 제한 중 하나에 도달하면 ClickHouse는 join_overflow_mode 설정에 따라 동작합니다.

예시

예시:
SELECT
    CounterID,
    hits,
    visits
FROM
(
    SELECT
        CounterID,
        count() AS hits
    FROM test.hits
    GROUP BY CounterID
) ANY LEFT JOIN
(
    SELECT
        CounterID,
        sum(Sign) AS visits
    FROM test.visits
    GROUP BY CounterID
) USING CounterID
ORDER BY hits DESC
LIMIT 10
┌─CounterID─┬───hits─┬─visits─┐
│   1143050 │ 523264 │  13665 │
│    731962 │ 475698 │ 102716 │
│    722545 │ 337212 │ 108187 │
│    722889 │ 252197 │  10547 │
│   2237260 │ 196036 │   9522 │
│  23057320 │ 147211 │   7689 │
│    722818 │  90109 │  17847 │
│     48221 │  85379 │   4652 │
│  19762435 │  77807 │   7026 │
│    722884 │  77492 │  11056 │
└───────────┴────────┴────────┘
마지막 수정일 2026년 6월 10일