Analyzer
Analyzer
ClickHouse 버전 24.3부터 analyzer가 기본으로 활성화됐어요. 동작 방식에 대한 더 자세한 내용은 여기에서 읽을 수 있어요. 버전 26.9부터 analyzer는 필수예요: enable_analyzer 설정은 더 이상 쓰지 않고, 0으로 설정하려 하면 거부되며, ClickHouse가 24.3 이전에 사용하던 쿼리 분석은 더 이상 지원되지 않아요. 아래 나열된 비호환성은 이전 분석이 어떻게 달랐는지 설명해서, 그에 맞게 쓴 쿼리를 수정할 수 있게 해줘요; 그 동작을 관찰하려면 26.9보다 오래된 ClickHouse 버전으로 쿼리를 실행해보세요.
출처: 문서
본문
알려진 비호환성 (Known incompatibilities)
많은 버그를 수정하고 새로운 최적화를 도입했지만, ClickHouse 동작에 일부 큰 변화도 도입해요. analyzer에 맞게 쿼리를 어떻게 다시 작성할지 결정하려면 다음 변경 사항을 읽어주세요.
유효하지 않은 쿼리는 더 이상 최적화되지 않습니다
이전 쿼리 계획 인프라는 쿼리 검증 단계 전에 AST 수준 최적화를 적용했어요. 최적화가 초기 쿼리를 유효하고 실행 가능한 것으로 다시 쓸 수 있었어요. analyzer에서는 쿼리 검증이 최적화 단계보다 먼저 이뤄져요. 즉 이전에는 실행 가능했던 유효하지 않은 쿼리가 이제는 지원되지 않아요. 이런 경우 쿼리를 수동으로 고쳐야 해요.
예제 1
다음 쿼리는 집계 후에 toString(number)만 사용 가능한데도 프로젝션 목록에서 number 컬럼을 사용해요. 이전 analyzer에서는 GROUP BY toString(number)가 GROUP BY number로 최적화되어 쿼리를 유효하게 만들었어요.
SELECT number
FROM numbers(1)
GROUP BY toString(number)
예제 2
이 쿼리에서도 같은 문제가 발생해요. 집계 후 다른 키와 함께 number 컬럼을 사용하고 있어요. 이전 쿼리 analyzer는 number > 5 필터를 HAVING 절에서 WHERE 절로 옮겨서 이 쿼리를 고쳤어요.
SELECT
number % 2 AS n,
sum(number)
FROM numbers(10)
GROUP BY n
HAVING number > 5
쿼리를 고치려면 비집계 컬럼에 적용되는 모든 조건을 표준 SQL 구문에 맞게 WHERE 섹션으로 옮겨야 해요:
SELECT
number % 2 AS n,
sum(number)
FROM numbers(10)
WHERE number > 5
GROUP BY n
마이그레이션 보조로, analyzer는 비집계 AND-접속사에 대해 이전 HAVING-to-WHERE 재작성을 복제할 수 있어요. analyzer_compatibility_allow_non_aggregate_in_having = 1을 활성화하면 이 동작을 선택할 수 있어요. 이 설정은 ClickHouse 26.7부터 사용 가능해요. WITH CUBE, WITH ROLLUP, WITH TOTALS, GROUPING SETS에서는 이 설정이 무시돼요. 집계, grouping, 또는 비결정적 함수를 포함한 접속사는 HAVING에 남아요; 어떤 접속사라도 윈도우 함수나 상태 유지 함수(예: rowNumberInBlock)를 포함하면, 레거시 동작에 맞춰 전체 HAVING에 대해 재작성이 비활성화돼요.
유효하지 않은 쿼리로 CREATE VIEW 만들기
analyzer는 항상 타입 검사를 수행해요. 이전에는 유효하지 않은 SELECT 쿼리로 VIEW를 만들 수 있었어요. 그러면 첫 번째 SELECT나 INSERT(MATERIALIZED VIEW의 경우) 때 실패했어요. 이제는 이런 방식으로 VIEW를 만들 수 없어요.
예제
CREATE TABLE source (data String)
ENGINE=MergeTree
ORDER BY tuple();
CREATE VIEW some_view
AS SELECT JSONExtract(data, 'test', 'DateTime64(3)')
FROM source;
JOIN 절의 알려진 비호환성
프로젝션 컬럼을 사용하는 JOIN
SELECT 목록의 별칭은 기본적으로 JOIN USING 키로 사용할 수 없어요. 새 설정 analyzer_compatibility_join_using_top_level_identifier를 활성화하면 JOIN USING이 왼쪽 테이블의 컬럼을 직접 사용하는 대신 SELECT 쿼리의 프로젝션 목록에 있는 표현식을 기준으로 식별자를 해석하도록 동작을 바꿔요. 예를 들어:
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로 설정하면 조인 조건이 t1.a + 1 = t2.b로 해석되어 이전 버전의 동작과 일치해요. 결과는 2, 'two'가 돼요. 설정이 false이면 조인 조건은 기본적으로 t1.b = t2.b이고, 쿼리는 2, 'one'을 반환해요. b가 t1에 없으면 쿼리가 오류로 실패해요.
JOIN USING과 ALIAS/MATERIALIZED 컬럼의 동작 변화
analyzer에서 ALIAS 또는 MATERIALIZED 컬럼을 포함하는 JOIN USING 쿼리에 *를 사용하면 기본적으로 해당 컬럼들이 결과 집합에 포함돼요. 예를 들어:
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);
analyzer에서 이 쿼리의 결과는 두 테이블의 id와 함께 payload 컬럼을 포함해요. 반면 이전 analyzer는 특정 설정(asterisk_include_alias_columns 또는 asterisk_include_materialized_columns)이 활성화되어 있을 때만 이 ALIAS 컬럼들을 포함했고, 컬럼이 다른 순서로 나타날 수도 있었어요. 특히 기존 쿼리를 analyzer로 마이그레이션할 때 일관되고 예상 가능한 결과를 보장하려면 * 대신 SELECT 절에서 컬럼을 명시적으로 지정하는 것이 좋아요.
USING 절 컬럼의 타입 수정자 처리
analyzer에서 USING 절에 지정된 컬럼의 공통 상위 타입 결정 규칙이, 특히 LowCardinality와 Nullable 같은 타입 수정자를 다룰 때 더 예측 가능한 결과를 내도록 표준화됐어요.
LowCardinality(T)와T:LowCardinality(T)타입 컬럼을T타입 컬럼과 조인하면 결과 공통 상위 타입은T가 되어LowCardinality수정자를 효과적으로 버려요.Nullable(T)와T:Nullable(T)타입 컬럼을T타입 컬럼과 조인하면 결과 공통 상위 타입은Nullable(T)가 되어 nullable 속성이 보존돼요.
예를 들어:
SELECT id, toTypeName(id)
FROM VALUES('id LowCardinality(String)', ('a')) AS t1
FULL OUTER JOIN VALUES('id String', ('b')) AS t2
USING (id);
이 쿼리에서 id의 공통 상위 타입은 String으로 결정되어 t1의 LowCardinality 수정자가 버려져요.
프로젝션 컬럼 이름 변경
프로젝션 이름 계산 중에는 별칭이 대체되지 않아요.
SELECT
1 + 1 AS x,
x + 1
FORMAT PrettyCompact
24.3 이전에는 두 번째 컬럼이 대체된 별칭으로 이름 붙었어요:
┌─x─┬─plus(plus(1, 1), 1)─┐
1. │ 2 │ 3 │
└───┴─────────────────────┘
analyzer는 이름에 별칭을 유지해요:
┌─x─┬─plus(x, 1)─┐
1. │ 2 │ 3 │
└───┴────────────┘
호환되지 않는 함수 인자 타입
analyzer에서 타입 추론은 초기 쿼리 분석 중에 일어나요. 이 변화는 타입 검사가 단락 평가(short-circuit evaluation)보다 먼저 수행된다는 뜻이에요. 따라서 if 함수의 인자는 항상 공통 상위 타입을 가져야 해요. 예를 들어 다음 쿼리는 There is no supertype for types Array(UInt8), String because some of them are Array and some of them are not와 함께 실패해요:
SELECT toTypeName(if(0, [2, 3, 4], 'String'))
이질적인 클러스터 (Heterogeneous clusters)
analyzer는 클러스터의 서버 간 통신 프로토콜을 크게 바꿔요. 따라서 analyzer를 사용하는지에 대해 서로 동의하지 않는 서버들 간에는 분산 쿼리를 실행할 수 없어요 — 26.9보다 오래된 서버 클러스터는 서로 다른 enable_analyzer 설정 값을 갖는다는 뜻이에요. 26.10 이상 버전의 서버에는 다른 쿼리 분석이 남아 있지 않아서, 이전 initiator가 보내는 값을 무시하고 analyzer로 쿼리를 분석해요. 두 분석은 결과 컬럼 이름을 같은 방식으로 짓지 않아서, initiator가 컬럼 이름으로 샤드가 반환한 블록을 일치시키므로, 그런 쿼리는 initiator에서 NOT_FOUND_COLUMN_IN_BLOCK로 실패할 수 있어요 — 예를 들어 비정규 대소문자로 쓴 함수(hostname())를 선택하고 analyzer가 이를 정규 이름(hostName())으로 해석할 때. 따라서 여전히 이전 쿼리 분석으로 실행 중인 클러스터는 서버 중 하나가 26.10으로 업그레이드되기 전에 모든 서버에서 enable_analyzer = 1을 설정해야 해요.
지원되지 않는 기능 (Unsupported features)
analyzer가 현재 지원하지 않는 기능 목록은 다음과 같아요:
- Annoy index.
- Hypothesis index. 여기에서 작업 중이에요.
클라우드 마이그레이션 (Cloud Migration)
새로운 기능·성능 최적화를 지원하기 위해 analyzer가 현재 비활성화된 모든 인스턴스에서 활성화하고 있어요. 이 변화는 더 엄격한 SQL 스코프 규칙을 강제하므로, 고객이 표준에 맞지 않는 쿼리를 수동으로 업데이트해야 해요.
마이그레이션 워크플로
normalized_query_hash로system.query_log를 필터링해 쿼리를 식별해요:
SELECT query
FROM clusterAllReplicas(default, system.query_log)
WHERE normalized_query_hash='{hash}'
LIMIT 1
SETTINGS skip_unavailable_shards=1
- 쿼리가 의존하는 이전 분석의 식별자 해석을 복원하는 호환성 설정을 추가해 analyzer로 쿼리를 실행해요.
SETTINGS
analyzer_compatibility_join_using_top_level_identifier=1
- 쿼리를 리팩터링하고, 결과가 마이그레이션 전 쿼리가 생산한 출력과 일치하는지 검증해요.
내부 테스트 중에 가장 자주 겪는 비호환성들을 참고하세요.
알 수 없는 표현식 식별자 (Unknown expression identifier)
오류: Unknown expression identifier ... in scope ... (UNKNOWN_IDENTIFIER). 예외 코드: 47
원인: 계산된 별칭을 필터에서 참조하는 것, 모호한 서브쿼리 프로젝션, "동적" CTE 스코프 지정 같은 비표준적·허용적 레거시 동작에 의존하는 쿼리가 이제 올바르게 유효하지 않은 것으로 식별되어 즉시 거부돼요.
해결: 아래처럼 SQL 패턴을 업데이트하세요.
- 필터 로직: 결과를 필터링한다면 로직을 WHERE에서 HAVING으로 옮기고, 원본 데이터를 필터링한다면 WHERE에서 표현식을 복제하세요.
- 서브쿼리 스코프: 외부 쿼리가 필요한 모든 컬럼을 명시적으로 선택하세요.
- JOIN 키: 키가 별칭이면 USING 대신 전체 표현식과 함께 ON을 사용하세요.
- 외부 쿼리에서 Subquery/CTE 자체의 별칭을 참조하지, 그 안의 테이블을 참조하지 마세요.
GROUP BY의 비집계 컬럼 (Non-Aggregated Columns in GROUP BY)
오류: Column ... is not under aggregate function and not in GROUP BY keys (NOT_AN_AGGREGATE). 예외 코드: 215
원인: 이전 analyzer는 GROUP BY 절에 없는 컬럼 선택을 허용했어요(종종 임의의 값을 골랐음). analyzer는 표준 SQL을 따릅니다: 선택된 모든 컬럼은 집계이거나 그룹핑 키여야 해요.
해결: 컬럼을 any(), argMax()로 감싸거나 GROUP BY에 추가하세요.
/* ORIGINAL QUERY */
-- device_id is ambiguous
SELECT user_id, device_id FROM table GROUP BY user_id
/* FIXED QUERY */
SELECT user_id, any(device_id) FROM table GROUP BY user_id
-- OR
SELECT user_id, device_id FROM table GROUP BY user_id, device_id
HAVING의 비집계 컬럼 (Non-Aggregated Columns in HAVING)
오류: Column ... is not under aggregate function and not in GROUP BY keys (NOT_AN_AGGREGATE). 예외 코드: 215
원인: 이전 analyzer는 HAVING의 비집계 AND-접속사를 WHERE로 조용히 옮겨 집계 전 필터처럼 취급했어요. analyzer는 표준 SQL을 따릅니다: HAVING은 집계 키와 집계 함수만 참조할 수 있어요.
해결: 술어를 HAVING에서 WHERE로 수동으로 옮기거나, 레거시 재작성을 복원하는 마이그레이션 보조로 analyzer_compatibility_allow_non_aggregate_in_having = 1(ClickHouse 26.7부터 사용 가능)을 활성화하세요. 호환성 설정은 WITH CUBE, WITH ROLLUP, WITH TOTALS, GROUPING SETS에서는 무시돼요. 집계, grouping, 또는 비결정적 함수를 포함한 접속사는 HAVING에 남아요; 어떤 접속사라도 윈도우 함수나 상태 유지 함수(예: rowNumberInBlock)를 포함하면, 레거시 동작에 맞춰 전체 HAVING에 대해 재작성이 비활성화돼요.
/* 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;
중복 CTE 이름 (Duplicate CTE names)
오류: CTE with name ... already exists (MULTIPLE_EXPRESSIONS_FOR_ALIAS). 예외 코드: 179
원인: 이전 analyzer는 같은 이름의 여러 Common Table Expression(WITH …) 정의를 허용했고, 나중 정의가 이전 정의를 가렸어요. analyzer는 기본적으로 이 모호함을 거부해요.
해결: 중복 CTE 이름이 고유하도록 바꾸세요. 마이그레이션 보조로, analyzer_compatibility_allow_cte_redefinition = 1(ClickHouse 26.10부터 사용 가능)을 활성화하면 레거시 동작을 복원해요: 참조는 그 순간 해석되지 않는 이름의 최신 정의에 바인딩되므로, 재정의가 이전 정의를 읽을 수 있고 쿼리 본문은 마지막 정의를 읽어요. 제한사항: MATERIALIZED로 선언된 CTE와 WITH RECURSIVE 절의 CTE는 설정을 활성화해도 재정의할 수 없어요. 이전 analyzer와 다른 한 가지 형태가 있어요: 이름 정의 두 개 사이에 선언된 CTE도 마지막 정의에 바인딩되는데, 이전 analyzer는 선언 시점에 보이는 정의에 바인딩했어요.
/* 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;
모호한 컬럼 식별자 (Ambiguous column identifiers)
오류: JOIN [JOIN TYPE] ambiguous identifier ... (AMBIGUOUS_IDENTIFIER). 예외 코드: 207
원인: 쿼리가 JOIN 내 여러 테이블에 있는 컬럼 이름을 원본 테이블 지정 없이 참조해요. 이전 analyzer는 내부 로직에 기반해 컬럼을 추측하는 경우가 많았지만, analyzer는 명시적인 이름을 요구해요.
해결: 컬럼을 table_alias.column_name으로 완전히 한정하세요.
/* ORIGINAL QUERY */
SELECT table1.ID AS ID FROM table1, table2 WHERE ID...
/* FIXED QUERY */
SELECT table1.ID AS ID_RENAMED FROM table1, table2 WHERE ID_RENAMED...
FINAL의 잘못된 사용 (Invalid usage of FINAL)
오류: Table expression modifiers FINAL are not supported for subquery... 또는 Storage ... doesn't support FINAL (UNSUPPORTED_METHOD). 예외 코드: 1, 181
원인: FINAL은 테이블 스토리지(구체적으로는 [Shared]ReplacingMergeTree)를 위한 수정자예요. analyzer는 FINAL이 다음에 적용되면 거부해요:
- 서브쿼리나 파생 테이블 (예:
FROM (SELECT …) FINAL). - 이를 지원하지 않는 테이블 엔진 (예: SharedMergeTree).
해결: FINAL을 서브쿼리 안의 원본 테이블에만 적용하거나, 엔진이 지원하지 않으면 제거하세요.
/* ORIGINAL QUERY */
SELECT * FROM (SELECT * FROM my_table) AS subquery FINAL ...
/* FIXED QUERY */
SELECT * FROM (SELECT * FROM my_table FINAL) AS subquery ...
countDistinct() 함수 대소문자 비민감성
오류: Function with name countdistinct does not exist (UNKNOWN_FUNCTION). 예외 코드: 46
원인: 함수 이름은 analyzer에서 대소문자를 구분하거나 엄격하게 매핑돼요. countdistinct(모두 소문자)는 더 이상 자동으로 해석되지 않아요.
해결: 표준 countDistinct(camelCase) 또는 ClickHouse 고유의 uniq를 사용하세요.