마이그레이션 가이드: SQL 호환 모드
마이그레이션 가이드: SQL 호환 모드
Druid 32.0.0부터 제거된 레거시 null 처리 설정과, ANSI SQL 호환 null 처리 모드로 전환하는 전략을 소개해요. 수집 시 null 변환, 빈 문자열 강제 변환, SQL 쿼리 재작성 방법을 다뤄요.
출처: 문서
본문
Apache Druid 32.0.0에서 ANSI SQL 표준과 호환되지 않는 레거시 구성들이 제거됐어요.
이 구성들은 다음과 같아요:
- druid.generic.useDefaultValueForNull
- druid.expressions.useStrictBooleans
- druid.generic.useThreeValueLogicForNativeFilters
이 가이드는 애플리케이션에서 레거시 Druid null 처리 동작에 의존하는 Druid 운영자가 Druid 32.0.0 이상으로 전환하기 위한 전략을 제공해요.
SQL 호환 null 처리
Druid 28.0.0부터 Druid는 기본적으로 ANSI SQL 호환 null 처리 모드로 segment를 작성하며, Druid 32.0.0에서는 더 이상 이를 구성할 수 없어요. 이는 레거시 동작의 변경으로, Druid가 문자열 dimension에서 null 값을 빈 문자열과 구분해 저장하고, 숫자 dimension에서 0과 구분해 저장한다는 의미예요.
ANSI SQL 표준은 null에 대한 모든 비교를 unknown으로 정의하므로 이는 애플리케이션 동작에 영향을 줄 수 있어요. 이 삼값 논리(three-valued logic)에 따르면 x <> 'some value' 는 null이 아닌 값만 반환해요.
Null handling 튜토리얼을 따라 Druid에서 null 처리가 어떻게 동작하는지 배워 보세요.
레거시 null 처리와 이값 필터 논리
Druid 28.0.0 이전에는 Druid가 null 대신 기본값을 저장하는 레거시 모드를 기본으로 했어요. 이 모드에서 Druid는 수집 시점에 다음 특성을 가진 segment를 만들었어요:
- 문자열 컬럼은 빈 문자열 '' 과 null 을 구분할 수 없었어요. 따라서 Druid는 두 값을 상호 교환 가능한 것으로 취급했어요.
- 숫자 컬럼은 null 값 행을 표현할 수 없었어요. 따라서 Druid는 null 대신 0 을 저장했어요.
SQL 호환 모드로 마이그레이션
비즈니스 로직이 레거시 모드의 동작에 의존한다면, Druid를 ANSI SQL 호환 null 처리 모드로 운영하기 위한 다음 옵션이 있어요:
- 들어오는 데이터를 수정해 null을 피하거나 빈 문자열을 피해 레거시 모드와 같은 쿼리 동작을 얻어요. 이는 수집 SQL 쿼리와 수집 spec을 수정해 null이나 빈 문자열을 처리한다는 뜻이에요. 예를 들어 문자열 컬럼의 null을 빈 문자열로, 숫자 컬럼의 null을 0으로 바꿔요. 다만 기존 쿼리가 Druid가 레거시 모드인 것처럼 동작해야 한다는 의미예요. null 값 보존이 중요하지 않다면 좋은 선택이에요.
- null 값을 보존하고 모든 SQL 쿼리를 ANSI SQL 호환이 되도록 업데이트해요. 이는 null을 그대로 둔 채 들어오는 데이터를 보존할 수 있다는 뜻이에요. 다만 영향을 받는 클라이언트 측 쿼리를 ANSI SQL 호환으로 다시 작성해야 해요. null 값 보존 요구사항이 있다면 이 옵션을 선택하세요.
수집 시점에 null 값 대체
Druid 내에서 null 값을 보존할 필요가 없다면, 수집 시점의 transform을 사용해 null을 다른 값으로 대체할 수 있어요.
다음 입력 데이터를 고려해 보세요:
{"time":"2024-01-01T00:00:00.000Z","string_example":"my_string","number_example":99}{"time":"2024-01-02T00:00:00.000Z","string_example":"","number_example":0}{"time":"2024-01-03T00:00:00.000Z","string_example":null,"number_example":null}
다음 예시는 수집 시점에 COALESCE 와 NVL 을 사용해 Druid에서 null 값을 피하는 방법을 보여줘요:
- SQL-based batch
- JSON-based batch
REPLACE INTO "no_nulls_example" OVERWRITE ALLWITH "ext" AS ( SELECT * FROM TABLE( EXTERN( '{"type":"inline","data":"{\"time\":\"2024-01-01T00:00:00.000Z\",\"string_example\":\"my_string\",\"number_example\":99}\n{\"time\":\"2024-01-02T00:00:00.000Z\",\"string_example\":\"\",\"number_example\":0}\n{\"time\":\"2024-01-03T00:00:00.000Z\",\"string_example\":null,\"number_example\":null}"}', '{"type":"json"}' ) ) EXTEND ("time" VARCHAR, "string_example" VARCHAR, "number_example" BIGINT))SELECT TIME_PARSE("time") AS "__time", -- Replace any null string values with an empty string COALESCE("string_example",'') AS string_example, -- Replace any null numeric values with 0 NVL("number_example",0) AS number_exampleFROM "ext"PARTITIONED BY MONTH
{ "type": "index_parallel", "spec": { "ioConfig": { "type": "index_parallel", "inputSource": { "type": "inline", "data": "{\"time\":\"2024-01-01T00:00:00.000Z\",\"string_example\":\"my_string\",\"number_example\":99}\n{\"time\":\"2024-01-02T00:00:00.000Z\",\"string_example\":\"\",\"number_example\":0}\n{\"time\":\"2024-01-03T00:00:00.000Z\",\"string_example\":null,\"number_example\":null}" }, "inputFormat": { "type": "json" } }, "tuningConfig": { "type": "index_parallel", "partitionsSpec": { "type": "dynamic" } }, "dataSchema": { "dataSource": "inline_data_native", "timestampSpec": { "column": "time", "format": "iso" }, "dimensionsSpec": { "dimensions": [ "string_example", { "type": "long", "name": "number_example" } ] }, "granularitySpec": { "queryGranularity": "none", "rollup": false, "segmentGranularity": "MONTH" }, "transformSpec": { "transforms": [ { "type": "expression", "name": "string_example", "expression": "COALESCE(\"string_example\",'')" }, { "type": "expression", "name": "number_example", "expression": "NVL(\"number_example\",0)" } ] } } }}
Druid는 다음과 같이 null 값 없는 데이터를 수집해요:
| __time | string_example | number_example | | 2024-01-01T00:00:00.000Z | my_string | 99 | | 2024-01-02T00:00:00.000Z | empty | 0 | | 2024-01-03T00:00:00.000Z | empty | 0 |
수집 시점에 빈 문자열을 null로 강제 변환
레거시 모드에서 Druid는 동등 비교를 위해 빈 문자열을 null로 인식했어요. 쿼리가 빈 문자열을 null로 나타내는 데 의존한다면, NULLIF 를 사용해 수집 시점에 빈 문자열을 null로 강제 변환할 수 있어요.
예를 들어 다음 예시 입력 데이터를 고려해 보세요:
{"time":"2024-01-01T00:00:00.000Z","string_example":"my_string"}{"time":"2024-01-02T00:00:00.000Z","string_example":""}{"time":"2024-01-03T00:00:00.000Z","string_example":null}
레거시 모드에서 Druid는 세 번째 레코드에 빈 문자열을 작성했어요. 따라서 다음 쿼리는 2 를 반환했어요:
SELECT count(*)FROM "null_string"WHERE "string_example" IS NULL
SQL 호환 모드에서 Druid는 빈 문자열과 null을 구분하므로, 같은 쿼리는 1 을 반환해요. 다음 예시는 IS NULL 비교를 수용하기 위해 빈 문자열을 null로 강제 변환하는 방법을 보여줘요:
- SQL-based batch
- JSON-based batch
REPLACE INTO "null_string" OVERWRITE ALLWITH "ext" AS ( SELECT * FROM TABLE( EXTERN( '{"type":"inline","data":"{\"time\":\"2024-01-01T00:00:00.000Z\",\"string_example\":\"my_string\"}\n{\"time\":\"2024-01-02T00:00:00.000Z\",\"string_example\":\"\"}\n{\"time\":\"2024-01-03T00:00:00.000Z\",\"string_example\":null}"}', '{"type":"json"}' ) ) EXTEND ("time" VARCHAR, "string_example" VARCHAR))SELECT TIME_PARSE("time") AS "__time", NULLIF("string_example",'') AS "string_example"FROM "ext"PARTITIONED BY MONTH
{ "type": "index_parallel", "spec": { "ioConfig": { "type": "index_parallel", "inputSource": { "type": "inline", "data": "{\"time\":\"2024-01-01T00:00:00.000Z\",\"string_example\":\"my_string\"}\n{\"time\":\"2024-01-02T00:00:00.000Z\",\"string_example\":\"\"}\n{\"time\":\"2024-01-03T00:00:00.000Z\",\"string_example\":null}" }, "inputFormat": { "type": "json" } }, "tuningConfig": { "type": "index_parallel", "partitionsSpec": { "type": "dynamic" } }, "dataSchema": { "dataSource": "null_string", "timestampSpec": { "column": "time", "format": "iso" }, "transformSpec": { "transforms": [ { "type": "expression", "expression": "case_searched((\"string_example\" == ''),null,\"string_example\")", "name": "string_example" } ] }, "dimensionsSpec": { "dimensions": [ "string_example" ] }, "granularitySpec": { "queryGranularity": "none", "rollup": false, "segmentGranularity": "month" } } }}
Druid는 빈 문자열 없이 데이터를 다음과 같이 수집해요:
| __time | string_example | | 2024-01-01T00:00:00.000Z | my_string | | 2024-01-02T00:00:00.000Z | null | | 2024-01-03T00:00:00.000Z | null |
따라서 SELECT count(*) FROM "null_string" WHERE "string_example" IS NULL 은 2 를 반환해요.
쿼리를 SQL 호환으로 재작성
Druid 내 데이터에 null 값을 유지하고 싶다면, 다음 ANSI SQL 호환 쿼리 전략을 사용해 레거시 null 처리와 같은 결과를 얻을 수 있어요:
- null 값을 포함하도록 부등호 쿼리를 수정해요. 예를 들어 x <> 'some value' 는 (x <> 'some value' OR x IS NULL) 이 돼요.
- COALESCE 또는 NVL 을 사용해 null을 값으로 대체해요. 예를 들어 x + 1 은 NVL(numeric_value, 0)+1 이 돼요.
다음 Druid 데이터소스 null_example 을 고려해 보세요:
| __time | string_example | number_example | | 2024-01-01T00:00:00.000Z | my_string | 99 | | 2024-01-02T00:00:00.000Z | empty | 0 | | 2024-01-03T00:00:00.000Z | null | null |
Druid는 동등 비교에서 null 문자열을 제외해요. 예를 들어:
SELECT COUNT(*) AS count_exampleFROM "null_example"WHERE "string_example"<> 'my_string'
Druid는 1 을 반환해요. null이 unknown으로 간주되기 때문이에요: 값과 같지도, 같다고도 여겨지지 않아요.
결과에서 null 값을 세려면 OR 연산자를 사용할 수 있어요:
SELECT COUNT(*) AS count_exampleFROM "null_example"WHERE ("string_example"<> 'my_string') OR "string_example" IS NULL
Druid는 2 를 반환해요. 같은 결과를 얻으려면 null-safe 비교를 위해 IS DISTINCT FROM 을 사용할 수도 있어요:
SELECT COUNT(*) as count_exampleFROM "null_example"WHERE "string_example" IS DISTINCT FROM 'my_string'
마찬가지로 null에 대한 산술 연산자는 null을 반환해요. 예를 들어:
SELECT "number_example" + 1 AS additon_exampleFROM "null_example"
ANSI SQL 표준에 따라 null + 어떤 값도 null이므로, Druid는 다음을 반환해요:
| addition_example | | 100 | | 1 | | null |
산술에서 null을 피하려면 NVL 을 사용해요. 예를 들어:
SELECT NVL("number_example",0) + 1 AS additon_exampleFROM "null_example"
Druid는 다음을 반환해요:
| addition_example | | 100 | | 1 | | 1 |
더 알아보기 (Learn more)
- Null handling 튜토리얼 — 기본 null 처리 동작
- Null values — Druid의 null 값에 대한 설명
- Handling null values — Druid가 null 값을 저장하는 방식