배열(Arrays)

배열(Arrays)

Apache Druid는 VARCHAR, BIGINT, DOUBLE 타입에 대해 SQL 표준 ARRAY 타입 컬럼을 지원해요(네이티브 타입 ARRAY<STRING>, ARRAY<LONG>, ARRAY<DOUBLE>). 이 문서에서는 ARRAY 타입 컬럼의 삽입, 필터링, 그룹화 동작을 설명드릴게요.

출처: 문서

본문

Apache Druid는 VARCHAR, BIGINT, DOUBLE 타입에 대해 SQL 표준 ARRAY 타입 컬럼을 지원합니다(네이티브 타입 ARRAY<STRING>, ARRAY<LONG>, ARRAY<DOUBLE>). 그 외의 더 복잡한 ARRAY 타입은 중첩 컬럼(nested columns)으로 저장해야 해요. Druid ARRAY 타입은 표준 배열과 상당히 다른 동작을 하는 다중값 차원(multi-value dimension)과는 구별됩니다.

이 문서는 ARRAY 타입 컬럼의 삽입, 필터링, 그룹화 동작을 설명합니다. SQL에서 ARRAY 컬럼과 타입에 사용할 수 있는 함수에 대한 추가 세부 사항은 Druid SQL data type 문서와 SQL array function reference를 참고하세요.

다음 섹션들은 3개의 배열 타입 컬럼을 포함하는 다음 예시 데이터에 기반해 삽입, 필터링, 그룹화 동작을 설명합니다:

{"timestamp": "2023-01-01T00:00:00", "label": "row1", "arrayString": ["a", "b"],  "arrayLong":[1, null,3], "arrayDouble":[1.1, 2.2, null]}
{"timestamp": "2023-01-01T00:00:00", "label": "row2", "arrayString": [null, "b"], "arrayLong":null,        "arrayDouble":[999, null, 5.5]}
{"timestamp": "2023-01-01T00:00:00", "label": "row3", "arrayString": [],          "arrayLong":[1, 2, 3],   "arrayDouble":[null, 2.2, 1.1]} 
{"timestamp": "2023-01-01T00:00:00", "label": "row4", "arrayString": ["a", "b"],  "arrayLong":[1, 2, 3],   "arrayDouble":[]}
{"timestamp": "2023-01-01T00:00:00", "label": "row5", "arrayString": null,        "arrayLong":[],          "arrayDouble":null}

배열 수집

네이티브 배치 및 스트리밍 수집

Apache Kafka 같은 네이티브 배치 또는 스트리밍 수집을 사용할 때, 타입 인식 스키마 발견(type-aware schema discovery)과 공유되는 "auto" 타입 차원 스키마를 사용해 배열을 수집할 수 있어요. TSV나 CSV 데이터에서 수집할 때는 inputFormat의 listDelimiter 필드로 배열 구분자를 지정할 수 있습니다. JSON 데이터는 배열 타입으로 수집되려면 JSON 배열로 형식화되어야 해요. JSON 데이터는 inputFormat 구성이 필요하지 않습니다.

다음은 이 문서에서 사용한 데이터의 네이티브 수집을 위한 dimensionsSpec 예시입니다:

"dimensions": [
  {
    "type": "auto",
    "name": "label"
  },
  {
    "type": "auto",
    "name": "arrayString"
  },
  {
    "type": "auto",
    "name": "arrayLong"
  },
  {
    "type": "auto",
    "name": "arrayDouble"
  }
],

SQL 기반 수집

배열은 SQL 기반 수집으로 삽입할 수 있습니다.

예시

REPLACE INTO "array_example" OVERWRITE ALL
WITH "ext" AS (
  SELECT *
  FROM TABLE(
    EXTERN(
      '{"type":"inline","data":"{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row1\", \"arrayString\": [\"a\", \"b\"],  \"arrayLong\":[1, null,3], \"arrayDouble\":[1.1, 2.2, null]}\n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row2\", \"arrayString\": [null, \"b\"], \"arrayLong\":null,        \"arrayDouble\":[999, null, 5.5]}\n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row3\", \"arrayString\": [],          \"arrayLong\":[1, 2, 3],   \"arrayDouble\":[null, 2.2, 1.1]} \n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row4\", \"arrayString\": [\"a\", \"b\"],  \"arrayLong\":[1, 2, 3],   \"arrayDouble\":[]}\n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row5\", \"arrayString\": null,        \"arrayLong\":[],          \"arrayDouble\":null}"}',
      '{"type":"json"}'
    )
  ) EXTEND (
    "timestamp" VARCHAR,
    "label" VARCHAR,
    "arrayString" VARCHAR ARRAY,
    "arrayLong" BIGINT ARRAY,
    "arrayDouble" DOUBLE ARRAY
  )
)
SELECT
  TIME_PARSE("timestamp") AS "__time",
  "label",
  "arrayString",
  "arrayLong",
  "arrayDouble"
FROM "ext"
PARTITIONED BY DAY

배열은 롤업(rollup)을 위한 GROUP BY 키로도 사용될 수 있습니다:

REPLACE INTO "array_example_rollup" OVERWRITE ALL
WITH "ext" AS (
  SELECT *
  FROM TABLE(
    EXTERN(
      '{"type":"inline","data":"{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row1\", \"arrayString\": [\"a\", \"b\"],  \"arrayLong\":[1, null,3], \"arrayDouble\":[1.1, 2.2, null]}\n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row2\", \"arrayString\": [null, \"b\"], \"arrayLong\":null,        \"arrayDouble\":[999, null, 5.5]}\n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row3\", \"arrayString\": [],          \"arrayLong\":[1, 2, 3],   \"arrayDouble\":[null, 2.2, 1.1]} \n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row4\", \"arrayString\": [\"a\", \"b\"],  \"arrayLong\":[1, 2, 3],   \"arrayDouble\":[]}\n{\"timestamp\": \"2023-01-01T00:00:00\", \"label\": \"row5\", \"arrayString\": null,        \"arrayLong\":[],          \"arrayDouble\":null}"}',
      '{"type":"json"}'
    )
  ) EXTEND (
    "timestamp" VARCHAR,
    "label" VARCHAR,
    "arrayString" VARCHAR ARRAY,
    "arrayLong" BIGINT ARRAY,
    "arrayDouble" DOUBLE ARRAY
  )
)
SELECT
  TIME_PARSE("timestamp") AS "__time",
  "label",
  "arrayString",
  "arrayLong",
  "arrayDouble",
  COUNT(*) as "count"
FROM "ext"
GROUP BY 1,2,3,4,5
PARTITIONED BY DAY

arrayIngestMode

31보다 오래된 Druid 버전과 매끄러운 하위 호환 동작을 위해 arrayIngestMode 쿼리 컨텍스트 플래그가 있습니다. arrayIngestMode가 array일 때 SQL ARRAY 타입은 Druid 배열 컬럼을 사용해 저장됩니다. 이는 새 테이블에 권장되며 Druid 31 이상의 기본 구성입니다. arrayIngestMode가 mvd(legacy)일 때 SQL VARCHAR ARRAY는 암시적으로 ARRAY_TO_MV로 감싸집니다. 이로 인해 일반 스칼라 문자열과 같은 STRING 컬럼 타입을 사용하는 다중값 문자열로 저장됩니다. SQL BIGINT ARRAY와 DOUBLE ARRAY는 arrayIngestMode: mvd에서 로드할 수 없습니다. 이 모드는 권장되지 않으며 향후 릴리스에서 제거될 예정이지만, 하위 호환성을 위해 제공됩니다.

다음 표는 arrayIngestMode: array와 arrayIngestMode: mvd 사이의 SQL ARRAY 처리 차이를 요약합니다.

SQL type Stored type when arrayIngestMode: array (default) Stored type when arrayIngestMode: mvd
VARCHAR ARRAY ARRAY<STRING> multi-value STRING
BIGINT ARRAY ARRAY<LONG> not possible (validation error)
DOUBLE ARRAY ARRAY<DOUBLE> not possible (validation error)

어느 모드에서든 문자열 배열을 ARRAY_TO_MV로 명시적으로 감싸 다중값 문자열로 저장되게 할 수 있습니다. 배열을 포함하는 SQL INSERT 또는 REPLACE 문을 검증할 때, Druid는 그 문장이 같은 컬럼에 문자열 배열과 다중값 문자열을 섞게 되는지 확인합니다. 이 조건이 감지되면, 컬럼이 skipTypeVerification 컨텍스트 파라미터 아래에 이름이 지정되지 않는 한 문장은 검증에 실패합니다. 이 파라미터는 쉼표로 구분된 컬럼 이름 목록이거나 문자열 형태의 JSON 배열일 수 있어요. 이 검증은 같은 컬럼에 배열과 다중값 문자열이 실수로 섞이는 것을 방지하기 위해 수행됩니다.

배열 쿼리

필터링

모든 쿼리 타입과 필터링된 집계기(filtered aggregator)는 배열 타입 컬럼을 필터링할 수 있습니다. 필터는 배열 타입에 대해 다음 규칙을 따릅니다:

  • 모든 필터는 행의 전체 배열 값에 대해 매칭합니다.
  • 동등(equality)과 범위(range) 같은 네이티브 값 필터는 전체 배열 값에 대해 매칭하며, 이 네이티브 필터로 계획되는 SQL 구조도 마찬가지입니다.
  • IS NULL 필터는 전체 배열 값이 null인 행과 매칭합니다.
  • ARRAY_CONTAINS, ARRAY_OVERLAP 같은 배열 전용 함수는 해당 함수가 지정한 동작을 따릅니다.
  • 그 외의 모든 필터는 ARRAY 타입을 직접 지원하지 않으며 쿼리 오류를 발생시킵니다.

예시: 동등(equality)

SELECT *
FROM "array_example"
WHERE arrayLong = ARRAY[1,2,3]
{"__time":"2023-01-01T00:00:00.000Z","label":"row3","arrayString":"[]","arrayLong":"[1,2,3]","arrayDouble":"[null,2.2,1.1]"}
{"__time":"2023-01-01T00:00:00.000Z","label":"row4","arrayString":"[\"a\",\"b\"]","arrayLong":"[1,2,3]","arrayDouble":"[]"}

예시: null

SELECT *
FROM "array_example"
WHERE arrayLong IS NULL
{"__time":"2023-01-01T00:00:00.000Z","label":"row2","arrayString":"[null,\"b\"]","arrayLong":null,"arrayDouble":"[999.0,null,5.5]"}

예시: 범위(range)

SELECT *
FROM "array_example"
WHERE arrayString >= ARRAY['a','b']
{"__time":"2023-01-01T00:00:00.000Z","label":"row1","arrayString":"[\"a\",\"b\"]","arrayLong":"[1,null,3]","arrayDouble":"[1.1,2.2,null]"}
{"__time":"2023-01-01T00:00:00.000Z","label":"row4","arrayString":"[\"a\",\"b\"]","arrayLong":"[1,2,3]","arrayDouble":"[]"}

예시: ARRAY_CONTAINS

SELECT *
FROM "array_example"
WHERE ARRAY_CONTAINS(arrayString, 'a')

그룹화

SQL이나 네이티브 groupBy 쿼리로 배열을 그룹화할 때, 그룹화는 표준 SQL 동작을 따르며 전체 배열을 단일 값으로 그룹화합니다. UNNEST 함수를 사용하면 개별 배열 요소로 그룹화할 수 있어요.

예시: 필터 없는 SQL 그룹화 쿼리

SELECT label, arrayString
FROM "array_example"
GROUP BY 1,2

결과:

{"label":"row1","arrayString":"[\"a\",\"b\"]"}
{"label":"row2","arrayString":"[null,\"b\"]"}
{"label":"row3","arrayString":"[]"}
{"label":"row4","arrayString":"[\"a\",\"b\"]"}
{"label":"row5","arrayString":null}

예시: 필터가 있는 SQL 그룹화 쿼리

SELECT label, arrayString
FROM "array_example"
WHERE arrayLong = ARRAY[1,2,3]
GROUP BY 1,2

결과:

{"label":"row3","arrayString":"[]"}
{"label":"row4","arrayString":"[\"a\",\"b\"]"}

예시: UNNEST

SELECT label, strings
FROM "array_example" CROSS JOIN UNNEST(arrayString) as u(strings)
GROUP BY 1,2
{"label":"row1","strings":"a"}
{"label":"row1","strings":"b"}
{"label":"row2","strings":null}
{"label":"row2","strings":"b"}
{"label":"row4","strings":"a"}
{"label":"row4","strings":"b"}

배열과 다중값 차원의 차이점

문자열 배열을 다중값 차원과 혼동하지 마세요. 배열과 다중값 차원은 서로 다른 컬럼 타입에 저장되며 쿼리 동작이 다릅니다. 필요하다면 MV_TO_ARRAY와 ARRAY_TO_MV 함수를 사용해 둘 사이를 변환할 수 있어요. 일반적으로 배열은 더 새롭고 강력한 기능이며 SQL 호환 동작을 가지므로, 가능할 때마다 배열을 사용할 것을 권장합니다.

수집 중 원하는 타입을 얻도록 주의하세요. 네이티브 배치나 Apache Kafka 같은 스트리밍 수집 같은 JSON 수집 스펙으로 수집을 수행해 배열을 얻으려면 차원 타입 auto를 사용하거나 useSchemaDiscovery를 활성화하세요. SQL 기반 수집을 수행할 때는 배열을 생성하는 쿼리를 작성하세요. 배열은 문자열이나 숫자를 포함할 수 있어요.

JSON 수집 스펙으로 수집을 수행해 다중값 차원을 얻으려면 차원 타입 string을 사용하고 useSchemaDiscovery를 활성화하지 마세요. SQL 기반 수집을 수행할 때는 배열을 ARRAY_TO_MV로 감싸면 다중값 차원을 얻을 수 있습니다. 다중값 차원은 문자열만 포함할 수 있어요.

어떤 타입을 가지고 있는지는 INFORMATION_SCHEMA.COLUMNS 테이블을 확인해 알 수 있습니다:

SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'mytable'

배열은 ARRAY 타입이고, 다중값 문자열은 VARCHAR 타입입니다.

더 알아보기 (Learn more)

  • Druid SQL data types: ARRAY 타입을 포함한 SQL 데이터 타입.
  • SQL array functions: ARRAY 컬럼과 함께 사용할 수 있는 함수.