Null 처리 튜토리얼

Null 처리 튜토리얼 (Null handling tutorial)

Apache Druid에서 문자열(string)과 숫자(numeric) 컬럼에 대한 null 처리의 기본 개념을 소개해 드릴게요. 이 튜토리얼은 NULL 값을 가진 컬럼에 논리 NOT 연산을 사용하는 필터에 초점을 맞춰요.

출처: 문서

본문

사전 준비 (Prerequisites)

이 튜토리얼을 시작하기 전에 Local quickstart에 설명된 대로 Apache Druid를 로컬 머신에 다운로드하고 실행해 주세요.

튜토리얼은 Query 뷰를 사용해 데이터를 수집하고 쿼리하는 데 익숙하다고 가정해요.

또한 튜토리얼은 null 처리를 위한 기본 설정을 변경하지 않았다고 가정해요.

Null 값이 있는 데이터 로드하기 (Load data with null values)

튜토리얼의 샘플 데이터에는 다음과 같이 문자열과 숫자 컬럼에 대한 null 값이 있어요:

{"date": "1/1/2024 1:02:00","title": "example_1","string_value": "some_value","numeric_value": 1}
{"date": "1/1/2024 1:03:00","title": "example_2","string_value": "another_value","numeric_value": 2}
{"date": "1/1/2024 1:04:00","title": "example_3","string_value": "", "numeric_value": null}
{"date": "1/1/2024 1:05:00","title": "example_4","string_value": null, "numeric_value": null}

Druid 콘솔에서 다음 쿼리를 실행해 데이터를 로드해 주세요:

REPLACE INTO "null_example" OVERWRITE ALL
WITH "ext" AS (
  SELECT *
  FROM TABLE(
    EXTERN(
      '{"type":"inline","data":"{\"date\": \"1/1/2024 1:02:00\",\"title\": \"example_1\",\"string_value\": \"some_value\",\"numeric_value\": 1}\n{\"date\": \"1/1/2024 1:03:00\",\"title\": \"example_2\",\"string_value\": \"another_value\",\"numeric_value\": 2}\n{\"date\": \"1/1/2024 1:04:00\",\"title\": \"example_3\",\"string_value\": \"\", \"numeric_value\": null}\n{\"date\": \"1/1/2024 1:05:00\",\"title\": \"example_4\",\"string_value\": null, \"numeric_value\": null}"}',
      '{"type":"json"}'
    )
  )
  EXTEND ("date" VARCHAR, "title" VARCHAR, "string_value" VARCHAR, "numeric_value" BIGINT)
)
SELECT
  TIME_PARSE("date", 'd/M/yyyy H:mm:ss') AS "__time",
  "title",
  "string_value",
  "numeric_value"
FROM "ext"
PARTITIONED BY DAY

Druid가 데이터 로드를 마친 뒤 다음 쿼리를 실행해 테이블을 확인해 주세요:

SELECT * FROM "null_example"

Druid가 다음을 반환해요:

__time title string_value numeric_value
2024-01-01T01:02:00.000Z example_1 some_value 1
2024-01-01T01:03:00.000Z example_2 another_value 2
2024-01-01T01:04:00.000Z example_3 empty null
2024-01-01T01:05:00.000Z example_4 null null

example 3의 빈 문자열 값과 example 4의 null 문자열 값의 차이에 주목해 주세요.

문자열 쿼리 예시 (String query example)

이 섹션의 쿼리들은 문자열에서의 null 처리를 보여 줘요. 다음 쿼리는 문자열 값이 some_value 가 아닌 행을 필터링해요:

SELECT COUNT(*)
FROM "null_example"
WHERE "string_value" != 'some_value'

Druid는 another_value 와 빈 문자열 "" 에 대해 2를 반환해요. null 값은 카운트되지 않아요.

null 값이 COUNT(*) 에는 포함되지만 컬럼 값의 카운트로는 포함되지 않는다는 점에 주목해 주세요:

SELECT "string_value", 
      COUNT(*) AS count_all_rows, 
      COUNT("string_value") AS count_values
FROM "inline_data"
GROUP BY 1

참고: 원문의 이 예시 쿼리에서 FROM "inline_data" 로 되어 있는데, 위에서 만든 데이터소스 이름은 null_example 이에요. 실행할 때는 FROM "null_example" 으로 바꿔 주세요.

Druid가 다음을 반환해요:

string_value count_all_rows count_values
null 1 0
empty 1 1
another_value 1 1
some_value 1 1

또한 GROUP BY 표현식이 null과 빈 문자열에 대해 구별된(distinct) 항목을 만든다는 점에 주목해 주세요.

null 외에 빈 문자열도 필터링하기 (Filter for empty strings in addition to null)

쿼리가 빈 문자열과 null 값을 동일하게 취급하는 데 의존한다면, 필터에서 OR 연산자를 사용할 수 있어요. 예를 들어 null 값 또는 빈 문자열이 있는 모든 행을 선택하려면:

SELECT *
FROM "null_example"
WHERE "string_value" IS NULL OR "string_value" = ''

Druid가 다음을 반환해요:

__time title string_value numeric_value
2024-01-01T01:04:00.000Z example_3 empty null
2024-01-01T01:05:00.000Z example_4 null null

또 다른 예시로, 빈 문자열을 카운트하고 싶지 않다면 FILTER 를 사용해 제외할 수 있어요. 예를 들면:

SELECT COUNT("string_value") FILTER(WHERE "string_value" <> '')
FROM "null_example"

Druid가 2를 반환해요. 빈 문자열과 null 값이 모두 제외돼요.

숫자 쿼리 예시 (Numeric query examples)

Druid는 숫자 비교에서 null 값을 카운트하지 않아요.

SELECT COUNT(*)
FROM "null_example"
WHERE "numeric_value" < 2

Druid가 1을 반환해요. example 3과 4의 null 값은 제외돼요.

추가로, null 값이 0처럼 동작하지 않는다는 점을 알아 두세요. 예를 들어:

SELECT numeric_value + 1
FROM "null_example"
WHERE "__time" > '2024-01-01 01:04:00.000Z'

Druid는 1이 아니라 null을 반환해요. 한 가지 옵션은 null 처리를 위해 COALESCE 함수를 사용하는 거예요. 예를 들면:

SELECT COALESCE(numeric_value, 0) + 1
FROM "null_example"
WHERE "__time" > '2024-01-01 01:04:00.000Z'

이 경우 Druid가 1을 반환해요.

수집 시점 필터링 (Ingestion time filtering)

수집 시점에도 동일한 null 처리 규칙이 적용돼요. 다음 쿼리는 예제 데이터를 WHERE 절로 필터링된 데이터로 교체해요:

REPLACE INTO "null_example" OVERWRITE ALL
WITH "ext" AS (
  SELECT *
  FROM TABLE(
    EXTERN(
      '{"type":"inline","data":"{\"date\": \"1/1/2024 1:02:00\",\"title\": \"example_1\",\"string_value\": \"some_value\",\"numeric_value\": 1}\n{\"date\": \"1/1/2024 1:03:00\",\"title\": \"example_2\",\"string_value\": \"another_value\",\"numeric_value\": 2}\n{\"date\": \"1/1/2024 1:04:00\",\"title\": \"example_3\",\"string_value\": \"\", \"numeric_value\": null}\n{\"date\": \"1/1/2024 1:05:00\",\"title\": \"example_4\",\"string_value\": null, \"numeric_value\": null}"}',
      '{"type":"json"}'
    )
  )
  EXTEND ("date" VARCHAR, "title" VARCHAR, "string_value" VARCHAR, "numeric_value" BIGINT)
)
SELECT
  TIME_PARSE("date", 'd/M/yyyy H:mm:ss') AS "__time",
  "title",
  "string_value",
  "numeric_value"
FROM "ext"
WHERE "string_value" != 'some_value'
PARTITIONED BY DAY

결과 데이터셋에는 행이 두 개만 포함돼요. Druid가 example 1(some_value)과 example 4(null)를 필터링했어요:

__time title string_value numeric_value
2024-01-01T01:03:00.000Z example_2 another_value 2
2024-01-01T01:04:00.000Z example_3 empty null

더 알아보기 (Learn more)

더 자세한 내용은 다음을 참고해 주세요: