> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-parallel-read-in-order-multi-part.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> ClickHouse 쿼리 분석기를 자세히 설명하는 페이지

# 분석기

ClickHouse 버전 `24.3`부터 분석기가 기본적으로 활성화되었습니다.
작동 방식에 대한 자세한 내용은 [여기](/ko/guides/clickhouse/performance-and-monitoring/understanding-query-execution-with-the-analyzer#analyzer)에서 확인할 수 있습니다.

버전 `26.9`부터 분석기는 필수입니다. `enable_analyzer` 설정은 더 이상 사용되지 않으며, 이를 `0`으로 설정하려는 시도는 거부되고, ClickHouse가 `24.3` 이전에 사용하던 쿼리 분석 방식은 더 이상 지원되지 않습니다. 아래에 나열된 비호환 항목은 이전 분석 방식이 어떻게 달랐는지를 설명하므로, 이전 방식에 맞춰 작성된 쿼리를 수정할 때 참고할 수 있습니다. 해당 동작을 직접 확인하려면 `26.9`보다 이전 버전의 ClickHouse에서 쿼리를 실행하십시오.

<h2 id="known-incompatibilities">
  알려진 비호환성
</h2>

많은 버그를 수정하고 새로운 최적화를 도입했지만, 그에 따라 ClickHouse 동작에도 일부 호환되지 않는 변경 사항이 생겼습니다. 아래 변경 사항을 읽고 분석기에 맞게 쿼리를 어떻게 재작성해야 하는지 확인하십시오.

<h3 id="invalid-queries-are-no-longer-optimized">
  잘못된 쿼리는 더 이상 최적화되지 않습니다
</h3>

이전 쿼리 계획 인프라에서는 쿼리 검증 단계 전에 AST 수준의 최적화를 적용했습니다.
이 최적화로 인해 원래 쿼리가 유효하고 실행 가능한 형태로 재작성될 수 있었습니다.

분석기에서는 최적화 단계에 앞서 쿼리 검증이 수행됩니다.
즉, 이전에는 실행할 수 있었던 잘못된 쿼리를 이제는 더 이상 지원하지 않습니다.
이러한 경우에는 쿼리를 수동으로 수정해야 합니다.

<h4 id="example-1">
  예시 1
</h4>

다음 쿼리는 집계 후 `toString(number)`만 사용할 수 있음에도 프로젝션 목록에서 컬럼 `number`를 사용합니다.
이전 분석기에서는 `GROUP BY toString(number)`가 `GROUP BY number,`로 최적화되어 해당 쿼리가 유효했습니다.

```sql theme={null}
SELECT number
FROM numbers(1)
GROUP BY toString(number)
```

<h4 id="example-2">
  예시 2
</h4>

이 쿼리에서도 동일한 문제가 발생합니다. `number` 컬럼은 다른 키로 집계한 뒤에 사용됩니다.
이전 쿼리 분석기는 `number > 5` 필터를 `HAVING` 절에서 `WHERE` 절로 옮겨 이 쿼리를 수정했습니다.

```sql theme={null}
SELECT
    number % 2 AS n,
    sum(number)
FROM numbers(10)
GROUP BY n
HAVING number > 5
```

쿼리를 수정하려면 표준 SQL 구문에 맞게 집계되지 않은 컬럼에 대한 모든 조건을 `WHERE` 절로 옮겨야 합니다:

```sql theme={null}
SELECT
    number % 2 AS n,
    sum(number)
FROM numbers(10)
WHERE number > 5
GROUP BY n
```

마이그레이션을 돕기 위해 분석기는 집계되지 않은 AND 결합 조건에 대해 이전의 `HAVING`-`WHERE` 재작성을 복제할 수 있습니다. 이 동작을 사용하려면 `analyzer_compatibility_allow_non_aggregate_in_having = 1`을 활성화하십시오. 이 설정은 ClickHouse `26.7`부터 사용할 수 있습니다. 이 설정은 `WITH CUBE`, `WITH ROLLUP`, `WITH TOTALS`, `GROUPING SETS`에서는 무시됩니다. aggregate, `grouping` 또는 비결정적 함수를 포함하는 결합 조건은 `HAVING`에 남습니다. 결합 조건 중 하나라도 윈도 함수 또는 상태 저장 함수(예: `rowNumberInBlock`)를 포함하면 전체 `HAVING`에 대한 재작성이 비활성화되며, 이는 레거시 동작과 일치합니다.

<h3 id="create-view-with-invalid-query">
  잘못된 쿼리로 `CREATE VIEW`
</h3>

분석기는 항상 타입 검사를 수행합니다.
이전에는 잘못된 `SELECT` 쿼리로 `VIEW`를 생성할 수 있었습니다.
이 경우 첫 번째 `SELECT` 또는 `INSERT` 시점에 실패했습니다(`MATERIALIZED VIEW`의 경우).

이제는 이런 방식으로 `VIEW`를 생성할 수 없습니다.

<h4 id="example-view">
  예시
</h4>

```sql theme={null}
CREATE TABLE source (data String)
ENGINE=MergeTree
ORDER BY tuple();

CREATE VIEW some_view
AS SELECT JSONExtract(data, 'test', 'DateTime64(3)')
FROM source;
```

<h3 id="known-incompatibilities-of-the-join-clause">
  `JOIN` 절의 알려진 비호환 사항
</h3>

<h4 id="join-using-column-from-projection">
  프로젝션의 컬럼을 사용한 `JOIN`
</h4>

기본적으로 `SELECT` 목록의 별칭은 `JOIN USING` 키로 사용할 수 없습니다.

새로운 설정인 `analyzer_compatibility_join_using_top_level_identifier`을 활성화하면 `JOIN USING`의 동작이 바뀌며, 왼쪽 테이블의 컬럼을 직접 사용하는 대신 `SELECT` 쿼리의 프로젝션 목록에 있는 표현식을 기준으로 식별자를 우선 해석합니다.

예를 들면:

```sql theme={null}
SELECT a + 1 AS b, t2.s
FROM VALUES('a UInt64, b UInt64', (1, 1)) AS t1
JOIN VALUES('b UInt64, s String', (1, 'one'), (2, 'two')) t2
USING (b);
```

`analyzer_compatibility_join_using_top_level_identifier`를 `true`로 설정하면, 이전 버전의 동작과 동일하게 join 조건이 `t1.a + 1 = t2.b`로 해석됩니다.
결과는 `2, 'two'`입니다.
설정이 `false`이면 join 조건은 기본적으로 `t1.b = t2.b`로 해석되며, 쿼리는 `2, 'one'`을 반환합니다.
`t1`에 `b`가 없으면 쿼리는 오류를 발생시키며 실패합니다.

<h4 id="changes-in-behavior-with-join-using-and-aliasmaterialized-columns">
  `JOIN USING`과 `ALIAS`/`MATERIALIZED` 컬럼의 동작 변경
</h4>

분석기에서는 `ALIAS` 또는 `MATERIALIZED` 컬럼이 포함된 `JOIN USING` 쿼리에서 `*`를 사용하면, 기본적으로 해당 컬럼도 결과 집합에 포함됩니다.

예시:

```sql theme={null}
CREATE TABLE t1 (id UInt64, payload ALIAS sipHash64(id)) ENGINE = MergeTree ORDER BY id;
INSERT INTO t1 VALUES (1), (2);

CREATE TABLE t2 (id UInt64, payload ALIAS sipHash64(id)) ENGINE = MergeTree ORDER BY id;
INSERT INTO t2 VALUES (2), (3);

SELECT * FROM t1
FULL JOIN t2 USING (payload);
```

분석기에서는 이 쿼리 결과에 두 테이블의 `id`와 함께 `payload` 컬럼도 포함됩니다.
반면 이전 분석기에서는 특정 설정(`asterisk_include_alias_columns` 또는 `asterisk_include_materialized_columns`)이 활성화된 경우에만 이러한 `ALIAS` 컬럼이 포함되었으며,
컬럼 순서도 달라질 수 있었습니다.

일관되고 예상 가능한 결과를 얻으려면, 특히 기존 쿼리를 분석기로 마이그레이션할 때는 `*`를 사용하는 대신 `SELECT` 절에서 컬럼을 명시적으로 지정하는 것이 좋습니다.

<h4 id="handling-of-type-modifiers-for-columns-in-using-clause">
  `USING` 절의 컬럼 타입 수정자 처리
</h4>

분석기에서는 `USING` 절에 지정된 컬럼의 공통 supertype을 결정하는 규칙이 표준화되어, 더 예측 가능한 결과를 얻을 수 있습니다.
특히 `LowCardinality` 및 `Nullable` 같은 타입 수정자를 다룰 때 그렇습니다.

* `LowCardinality(T)` and `T`: 타입이 `LowCardinality(T)`인 컬럼을 타입이 `T`인 컬럼과 조인하면, 결과 공통 supertype은 `T`가 되며 `LowCardinality` 수정자는 사실상 제거됩니다.
* `Nullable(T)` and `T`: 타입이 `Nullable(T)`인 컬럼을 타입이 `T`인 컬럼과 조인하면, 결과 공통 supertype은 `Nullable(T)`가 되어 널 허용 속성이 유지됩니다.

예시:

```sql theme={null}
SELECT id, toTypeName(id)
FROM VALUES('id LowCardinality(String)', ('a')) AS t1
FULL OUTER JOIN VALUES('id String', ('b')) AS t2
USING (id);
```

이 쿼리에서는 `id`의 공통 supertype이 `String`으로 결정되고, `t1`의 `LowCardinality` 수정자는 제거됩니다.

<h3 id="projection-column-names-changes">
  프로젝션 컬럼 이름 변경 사항
</h3>

프로젝션 이름을 계산할 때는 별칭이 치환되지 않습니다.

```sql theme={null}
SELECT
    1 + 1 AS x,
    x + 1
FORMAT PrettyCompact
```

`24.3` 이전에는 두 번째 컬럼의 이름이 치환된 별칭을 기준으로 지정되었습니다:

```text theme={null}
   ┌─x─┬─plus(plus(1, 1), 1)─┐
1. │ 2 │                   3 │
   └───┴─────────────────────┘
```

분석기는 이름에 별칭을 그대로 유지합니다:

```text theme={null}
   ┌─x─┬─plus(x, 1)─┐
1. │ 2 │          3 │
   └───┴────────────┘
```

<h3 id="incompatible-function-arguments-types">
  호환되지 않는 함수 인수 타입
</h3>

분석기에서는 초기 쿼리 분석 중에 타입 추론이 이루어집니다.
이 변경으로 인해 타입 검사는 단락 평가 전에 수행되므로, `if` 함수의 인수는 항상 공통 supertype을 가져야 합니다.

예를 들어, 다음 쿼리는 `There is no supertype for types Array(UInt8), String because some of them are Array and some of them are not`라는 오류와 함께 실패합니다:

```sql theme={null}
SELECT toTypeName(if(0, [2, 3, 4], 'String'))
```

<h3 id="heterogeneous-clusters">
  이기종 클러스터
</h3>

분석기는 클러스터 내 서버 간 통신 프로토콜을 크게 변경합니다. 따라서 분석기 사용 여부가 서로 일치하지 않는 서버 간에는 분산 쿼리를 실행할 수 없으며, `26.9` 이전 버전의 서버로 구성된 클러스터에서 이는 `enable_analyzer` 설정값이 서로 다른 서버를 의미합니다.

`26.10` 이상 버전의 서버에는 다른 쿼리 분석 방식이 남아 있지 않으므로, 이전 버전 initiator가 보낸 값을 무시하고 무조건 분석기로 쿼리를 분석합니다. 두 분석 방식은 결과 컬럼의 이름을 서로 다르게 지정하며, initiator는 세그먼트가 반환한 블록을 컬럼명으로 매칭하기 때문에 이러한 쿼리는 initiator에서 `NOT_FOUND_COLUMN_IN_BLOCK` 오류로 실패할 수 있습니다. 예를 들어 정규 표기가 아닌 대소문자로 작성된 FUNCTION(`hostname()`)을 조회하는 경우, 분석기는 이를 정규 이름(`hostName()`)으로 해석합니다. 따라서 이전 쿼리 분석 방식으로 동작 중인 클러스터는 서버 중 하나라도 `26.10`으로 업그레이드하기 전에 모든 서버에서 `enable_analyzer = 1`을 설정해야 합니다.

<h3 id="unsupported-features">
  지원되지 않는 기능
</h3>

현재 분석기에서 지원하지 않는 기능 목록은 다음과 같습니다:

* Annoy 인덱스.
* Hypothesis 인덱스. [여기](https://github.com/ClickHouse/ClickHouse/pull/48381)에서 작업이 진행 중입니다.

<h2 id="cloud-migration">
  Cloud 마이그레이션
</h2>

기능 및 성능 최적화를 지원하기 위해 현재 분석기가 비활성화되어 있는 모든 인스턴스에서 이를 활성화하고 있습니다. 이 변경으로 SQL 범위 규칙이 더 엄격해지므로, 규정을 준수하지 않는 쿼리는 사용자가 수동으로 업데이트해야 합니다.

<h3 id="migration-workflow">
  마이그레이션 워크플로
</h3>

1. `normalized_query_hash`를 사용해 `system.query_log`를 필터링하여 쿼리를 식별합니다:

```sql theme={null}
SELECT query 
FROM clusterAllReplicas(default, system.query_log)
WHERE normalized_query_hash='{hash}' 
LIMIT 1 
SETTINGS skip_unavailable_shards=1
```

2. 쿼리가 이전 분석 방식의 식별자 해석에 의존하는 경우, 이를 복원하는 compatibility 설정을 추가해 분석기로 쿼리를 실행합니다.

```sql theme={null}
SETTINGS
    analyzer_compatibility_join_using_top_level_identifier=1
```

3. 마이그레이션 이전에 쿼리가 생성했던 출력과 일치하는지 확인할 수 있도록 쿼리를 리팩터링하고 결과를 검증합니다.

내부 테스트에서 가장 자주 확인된 비호환성은 다음 내용을 참조하십시오.

<h3 id="unknown-expression-identifier">
  알 수 없는 표현식 식별자
</h3>

오류: `Unknown expression identifier ... in scope ... (UNKNOWN_IDENTIFIER)`. 예외 코드: 47

원인: 필터에서 계산된 별칭(alias)을 참조하거나, 모호한 서브쿼리 프로젝션, 또는 "동적" CTE 범위와 같은 비표준적이고 관대한 레거시 동작에 의존하는 쿼리는 이제 유효하지 않은 것으로 올바르게 판단되어 즉시 거부됩니다.

해결 방법: SQL 패턴을 다음과 같이 수정하십시오.

* 필터 로직: 결과를 기준으로 필터링하는 경우 WHERE의 로직을 HAVING으로 옮기고, 원본 데이터를 기준으로 필터링하는 경우 WHERE에 동일한 표현식을 다시 작성하십시오.
* 서브쿼리 범위: 바깥쪽 쿼리에 필요한 모든 컬럼을 명시적으로 선택하십시오.
* JOIN 키: 키가 별칭(alias)인 경우 USING 대신 전체 표현식을 포함한 ON을 사용하십시오.
* 바깥쪽 쿼리에서는 내부 테이블이 아니라 서브쿼리/CTE 자체의 별칭(alias)을 참조하십시오.

<h3 id="non-aggregated-columns-in-group-by">
  GROUP BY의 비집계 컬럼
</h3>

오류: `Column ... is not under aggregate function and not in GROUP BY keys (NOT_AN_AGGREGATE)`. 예외 코드: 215

원인: 이전 분석기는 GROUP BY 절에 없는 컬럼도 선택할 수 있게 허용했습니다(이 경우 임의의 값을 선택하는 일이 많았습니다). 분석기는 표준 SQL을 따릅니다. 즉, 선택한 모든 컬럼은 집계 함수이거나 그룹화 키여야 합니다.

해결 방법: 해당 컬럼을 `any()`, `argMax()`로 감싸거나 GROUP BY에 추가합니다.

```sql theme={null}
/* 원본 쿼리 */
-- device_id가 모호함
SELECT user_id, device_id FROM table GROUP BY user_id

/* 수정된 쿼리 */
SELECT user_id, any(device_id) FROM table GROUP BY user_id
-- 또는
SELECT user_id, device_id FROM table GROUP BY user_id, device_id
```

<h3 id="non-aggregated-columns-in-having">
  HAVING의 비집계 컬럼
</h3>

오류: `Column ... is not under aggregate function and not in GROUP BY keys (NOT_AN_AGGREGATE)`. 예외 코드: 215

원인: 이전 분석기는 `HAVING`의 비집계 AND 결합 조건을 사전 집계(pre-aggregation) 필터로 간주하고 자동으로 `WHERE`로 옮겼습니다. 분석기는 표준 SQL을 따릅니다. 즉, `HAVING`에서는 집계 키와 집계 함수만 참조할 수 있습니다.

해결 방법: 프레디케이트를 `HAVING`에서 `WHERE`로 수동으로 옮기거나, `analyzer_compatibility_allow_non_aggregate_in_having = 1`(ClickHouse `26.7`부터 사용 가능)을 활성화하여 마이그레이션을 돕기 위한 레거시 재작성을 복원하십시오. 이 compatibility setting은 `WITH CUBE`, `WITH ROLLUP`, `WITH TOTALS`, `GROUPING SETS`에서는 무시됩니다. 집계, `grouping`, 또는 비결정적 함수가 포함된 조건은 `HAVING`에 남아 있습니다. 조건 중 하나라도 윈도 함수 또는 상태 저장 함수(예: `rowNumberInBlock`)를 포함하면 전체 `HAVING`에 대한 재작성이 비활성화되며, 이는 레거시 동작과 일치합니다.

```sql theme={null}
/* ORIGINAL QUERY */
SELECT category, sum(value) FROM t GROUP BY category HAVING service = 'svc1';

/* FIXED QUERY */
SELECT category, sum(value) FROM t WHERE service = 'svc1' GROUP BY category;
```

<h3 id="duplicate-cte-names">
  중복된 CTE 이름
</h3>

오류: `CTE with name ... already exists (MULTIPLE_EXPRESSIONS_FOR_ALIAS)`. 예외 코드: 179

원인: 이전 분석기에서는 동일한 이름으로 여러 개의 공통 테이블 표현식(Common Table Expression, WITH ...)을 정의할 수 있었으며, 이때 나중에 정의된 것이 앞선 정의를 가렸습니다. 새 분석기는 기본적으로 이러한 모호성을 허용하지 않습니다.

해결 방법: 중복된 CTE의 이름을 서로 겹치지 않도록 변경하십시오. 마이그레이션 과정에서는 `analyzer_compatibility_allow_cte_redefinition = 1`(ClickHouse `26.10`부터 사용 가능)을 활성화하여 레거시 동작을 복원할 수 있습니다. 이 경우 참조는 해당 시점에 해석 중이지 않은 이름 중 가장 최근 정의에 바인딩됩니다. 따라서 재정의에서는 이전 정의를 읽을 수 있고, 쿼리 body에서는 마지막 정의를 읽습니다.

제한 사항: `MATERIALIZED`로 선언된 CTE와 `WITH RECURSIVE` 절 내의 CTE는 이 설정을 활성화하더라도 재정의할 수 없습니다. 또한 한 가지 경우에는 이전 분석기와 동작이 다릅니다. 동일한 이름의 두 정의 사이에 선언된 CTE 역시 마지막 정의에 바인딩되지만, 이전 분석기에서는 해당 CTE가 선언된 시점에 보이는 정의에 바인딩되었습니다.

```sql theme={null}
/* ORIGINAL QUERY */
WITH
  data AS (SELECT 1 AS id),
  data AS (SELECT id + 1 AS id FROM data) -- Redefined, reads the previous definition
SELECT * FROM data;

/* FIXED QUERY */
WITH
  raw_data AS (SELECT 1 AS id),
  processed_data AS (SELECT id + 1 AS id FROM raw_data)
SELECT * FROM processed_data;

/* LEGACY BEHAVIOR AS A MIGRATION AID */
WITH
  data AS (SELECT 1 AS id),
  data AS (SELECT id + 1 AS id FROM data)
SELECT * FROM data
SETTINGS analyzer_compatibility_allow_cte_redefinition = 1;
```

<h3 id="ambiguous-column-identifiers">
  모호한 컬럼 식별자
</h3>

오류: `JOIN [JOIN TYPE] ambiguous identifier ... (AMBIGUOUS_IDENTIFIER)` 예외 코드: 207

원인: 쿼리에서 JOIN에 포함된 여러 테이블에 있는 동일한 컬럼 이름을, 어느 테이블의 컬럼인지 지정하지 않은 채 참조합니다. 이전 분석기는 내부 로직을 기준으로 해당 컬럼을 추정하는 경우가 많았지만, 현재 분석기는 컬럼 이름을 명시적으로 지정해야 합니다.

해결 방법: 컬럼을 `table&#95;alias.column&#95;name` 형식으로 완전히 지정하십시오.

```sql theme={null}
/* 원본 쿼리 */
SELECT table1.ID AS ID FROM table1, table2 WHERE ID...

/* 수정된 쿼리 */
SELECT table1.ID AS ID_RENAMED FROM table1, table2 WHERE ID_RENAMED...
```

<h3 id="invalid-usage-of-final">
  FINAL의 잘못된 사용
</h3>

오류: `Table expression modifiers FINAL are not supported for subquery...` 또는 `Storage ... doesn't support FINAL` (`UNSUPPORTED_METHOD`). 예외 코드: 1, 181

원인: FINAL은 테이블 스토리지, 구체적으로 \[Shared]ReplacingMergeTree에 사용하는 수정자입니다. 분석기는 다음과 같은 경우 FINAL 적용을 허용하지 않습니다.

* 서브쿼리 또는 파생 테이블(예: FROM (SELECT ...) FINAL)
* FINAL을 지원하지 않는 테이블 엔진(예: SharedMergeTree)

해결 방법: FINAL은 서브쿼리 내부의 원본 테이블에만 적용하거나, 엔진이 지원하지 않으면 제거하십시오.

```sql theme={null}
/* 원본 쿼리 */
SELECT * FROM (SELECT * FROM my_table) AS subquery FINAL ...

/* 수정된 쿼리 */
SELECT * FROM (SELECT * FROM my_table FINAL) AS subquery ...
```

<h3 id="countdistinct-case-insensitivity">
  `countDistinct()` 함수의 대소문자 구분
</h3>

오류: `Function with name countdistinct does not exist (UNKNOWN_FUNCTION)`. 예외 코드: 46

원인: 함수 이름은 대소문자를 구분하며, 분석기에서 엄격하게 매핑됩니다. `countdistinct`(모두 소문자)는 더 이상 자동으로 인식되지 않습니다.

해결 방법: 표준 `countDistinct`(camelCase) 또는 ClickHouse 전용 `uniq`를 사용하십시오.
