스키마 설계 팁

스키마 설계 팁

Druid의 데이터 모델

일반적인 정보는 메인 수집 개요 페이지의 Druid schema model 문서를 확인해 보세요. 이 페이지의 나머지는 다른 종류의 시스템에서 온 사용자를 위한 팁과, 일반적인 팁·공통 관행을 다룹니다.

출처: 문서

본문

  • Druid 데이터는 전통적인 RDBMS의 테이블과 비슷한 datasource에 저장돼요.
  • Druid datasource는 롤업(rollup) 유무와 무관하게 수집할 수 있어요. 롤업을 활성화하면 Druid는 수집 중 데이터를 부분 집계해서 행 수를 줄이고, 저장 공간을 줄이며, 쿼리 성능을 높일 수 있어요. 롤업을 비활성화하면 Druid는 입력 데이터의 각 행마다 하나의 행을, 사전 집계 없이 저장합니다.
  • Druid의 모든 행에는 타임스탬프가 있어야 해요. 데이터는 항상 시간으로 파티셔닝되고, 모든 쿼리에는 시간 필터가 있습니다. 쿼리 결과는 분·시·일 같은 시간 버킷으로도 나눌 수 있어요.
  • Druid datasource의 타임스탬프 컬럼을 제외한 모든 컬럼은 dimension 또는 metric이에요. 이는 OLAP 데이터의 표준 명명 규칙을 따릅니다.
  • 전형적인 프로덕션 datasource는 수십에서 수백 개의 컬럼을 가져요.
  • Dimension 컬럼은 있는 그대로 저장되므로 쿼리 시점에 필터링·그룹화·집계할 수 있어요. 항상 단일 String, String 배열, 단일 Long, 단일 Double 또는 단일 Float 형태입니다.
  • Metric 컬럼은 사전 집계돼 저장되므로 쿼리 시점에만 집계할 수 있어요(필터링·그룹화는 불가). 보통 숫자(정수 또는 실수)로 저장되지만 HyperLogLog 스케치나 근사 분위수 스케치 같은 복잡한 객체로도 저장할 수 있어요. Metric은 롤업이 비활성화돼도 수집 시점에 구성할 수 있지만, 롤업이 활성화됐을 때 가장 유용합니다.

이런 시스템에서 온 경우

관계형 모델 (Relational model)

(Hive나 PostgreSQL 같은.)

Druid datasource는 일반적으로 관계형 데이터베이스의 테이블과 동일해요. Druid lookups는 데이터웨어하우스 스타일의 dimension 테이블처럼 동작할 수 있지만, 아래에서 보듯 가능하다면 비정규화(denormalization)가 자주 권장됩니다.

관계형 데이터 모델링의 일반 관행은 정규화(normalization)를 포함해요. 데이터를 여러 테이블로 나눠서 중복을 줄이거나 없애는 개념입니다. 예를 들어 "sales" 테이블에서 모범적인 관계형 모델링은 별도 "products" 테이블을 가리키는 외래 키인 "product id" 컬럼을 두는 것이고, "products" 테이블은 "product id", "product name", "product category" 컬럼을 갖습니다. 이렇게 하면 같은 제품을 가리키는 "sales" 테이블의 여러 행에 제품 이름·카테고리를 반복할 필요가 없어요.

반면 Druid에서는 쿼리 시점에 조인(join)이 필요 없는 완전히 플랫한 datasource를 쓰는 것이 일반적이에요. "sales" 테이블 예시에서 Druid에서는 별도 "products" 테이블 없이 "product_id", "product_name", "product_category"를 "sales" datasource에 dimension으로 직접 저장하는 것이 전형적입니다. 완전히 플랫한 스키마는 쿼리 시점의 조인 필요성을 없애서 성능을 크게 높여요. 추가 속도 향상으로, Druid의 쿼리 레이어가 압축된 사전 인코딩 데이터에서 직접 동작할 수 있게 됩니다. 다소 직관에 반하지만, 이는 정규화된 스키마에 비해 저장 공간을 크게 늘리지 않는데, Druid가 사전 인코딩으로 문자열 컬럼당 사실상 행당 정수 하나만 저장하기 때문이에요.

필요하다면 Druid datasource는 lookup을 사용해서 부분적으로 정규화할 수 있어요. lookup은 관계형 데이터베이스의 dimension 테이블과 대략 동등합니다. 쿼리 시점에는 관계형 DB에서처럼 JOIN 키워드를 쓰는 대신 Druid SQL LOOKUP 함수나 네이티브 lookup extraction 함수를 사용합니다. lookup 테이블은 메모리 사용을 늘리고 쿼리 시점에 더 많은 계산 오버헤드를 부과하므로, lookup 테이블을 업데이트하고 그 변경이 메인 테이블의 이미 수집된 행에 즉시 반영돼야 하는 경우에만 권장합니다.

Druid에서 관계형 데이터를 모델링하는 팁:

  • Druid datasource에는 기본 키나 고유 키가 없으니 그런 것은 생략하세요.
  • 가능하면 비정규화하세요. dimension/lookup 테이블을 주기적으로 업데이트하고 그 변경이 이미 수집된 데이터에 반영돼야 한다면, lookup으로 부분 정규화를 고려해 보세요.
  • 두 개의 큰 분산 테이블을 서로 조인해야 한다면 Druid에 데이터를 로드하기 전에 그 작업을 해 두세요. Druid는 두 datasource의 쿼리 시점 조인을 지원하지 않습니다. 각 lookup 테이블의 전체 복사본이 각 Druid 서버에 저장되므로 lookups도 여기서는 도움이 되지 않아요. 큰 테이블에는 적합하지 않습니다.
  • 사전 집계를 위해 롤업을 활성화할지, 아니면 롤업을 비활성화하고 기존 데이터를 그대로 로드할지 고려하세요. Druid의 롤업은 관계형 모델에서 요약 테이블(summary table)을 만드는 것과 비슷합니다.

시계열 모델 (Time series model)

(OpenTSDB나 InfluxDB 같은.)

시계열 데이터베이스와 비슷하게, Druid의 데이터 모델도 타임스탬프가 필요해요. Druid는 시계열 데이터베이스는 아니지만 시계열 데이터를 저장하기에 자연스러운 선택입니다. 유연한 데이터 모델 덕분에 같은 datasource 안에서 시계열 데이터와 비시계열 데이터를 모두 저장할 수 있어요.

Druid에서 시계열 데이터의 최상의 압축률·쿼리 성능을 얻으려면, 시계열 데이터베이스가 흔히 하듯 메트릭 이름으로 파티셔닝하고 정렬하는 것이 중요합니다. 자세한 내용은 Partitioning and sorting을 참고하세요.

Druid에서 시계열 데이터를 모델링하는 팁:

  • Druid는 데이터 포인트를 "시계열"의 일부로 생각하지 않아요. 대신 각 포인트를 수집·집계 측면에서 개별적으로 취급합니다.
  • 데이터 포인트가 속한 계열(series)의 이름을 나타내는 dimension을 만드세요. 이 dimension은 흔히 "metric" 또는 "name"이라고 불러요. "metric"이라는 이름의 dimension을 Druid 메트릭이라는 개념과 혼동하지 마세요. 최상의 성능을 위해 이것을 "dimensionsSpec"의 dimension 목록 맨 앞에 두세요(이렇게 하면 locality가 좋아지는데, 자세한 내용은 아래 partitioning and sorting 참고).
  • 데이터 포인트에 붙는 속성들을 위한 다른 dimension들을 만드세요. 시계열 데이터베이스 시스템에서는 이것을 흔히 "tags"라고 합니다.
  • 쿼리하고 싶은 집계 유형에 해당하는 metric을 만드세요. 전형적으로 "sum", "min", "max"가 포함됩니다(long, float, double 중 하나의 형태로). 백분위수나 분위수를 계산하고 싶다면 Druid의 근사 애그리게이터를 사용하세요.
  • 롤업 활성화를 고려해 보세요. 그러면 Druid가 여러 포인트를 Druid datasource의 한 행으로 결합할 수 있어요. 데이터가 자연 발생하는 시간 granularity와 다른 granularity로 저장하고 싶을 때 유용합니다. 같은 datasource에서 시계열·비시계열 데이터를 결합하고 싶을 때도 유용해요.
  • 미리 어떤 컬럼을 수집할지 모르겠다면 빈 dimensions 목록을 사용해서 dimension 컬럼 자동 감지를 트리거하세요.

로그 집계 모델 (Log aggregation model)

(Elasticsearch나 Splunk 같은.)

로그 집계 시스템과 비슷하게, Druid는 빠른 검색·필터링을 위한 역인덱스(inverted index)를 제공해요. Druid의 검색 기능은 일반적으로 이 시스템들보다 덜 발달했지만, 분석 기능은 더 발달돼 있습니다. Druid와 이 시스템들 사이의 주요 데이터 모델링 차이는, Druid에 데이터를 수집할 때 더 명시적이어야 한다는 점이에요. Druid 컬럼은 미리 정해진 타입을 갖습니다.

Druid에서 로그 데이터를 모델링하는 팁:

  • 미리 어떤 컬럼을 수집할지 모르겠다면 Druid가 스키마 자동 발견(schema auto-discovery)을 수행하게 할 수 있어요.
  • 중첩 데이터(nested data)가 있다면 중첩 컬럼(nested columns) 기능으로 수집하거나 flattenSpec으로 평탄화할 수 있습니다.
  • 로그 데이터에 주로 분석적 사용 사례가 있다면 롤업 활성화를 고려해 보세요. 그러면 Druid에서 개별 이벤트를 검색하는 능력은 잃지만, 상당한 압축률·쿼리 성능 향상을 얻을 수 있습니다.

일반 팁과 모범 사례

롤업 (Rollup)

Druid는 저장해야 하는 원시 데이터의 양을 최소화하기 위해 수집되는 데이터를 롤업할 수 있어요. 이는 요약 또는 사전 집계의 한 형태입니다. 자세한 내용은 수집 문서의 Rollup 섹션을 참고하세요.

파티셔닝과 정렬 (Partitioning and sorting)

데이터를 최적으로 파티셔닝·정렬하면 공간(footprint)과 성능에 상당한 영향을 줄 수 있어요. 자세한 내용은 수집 문서의 Partitioning 섹션을 참고하세요.

높은 카디널리티 컬럼을 위한 스케치 (Sketches for high cardinality columns)

사용자 ID나 다른 고유 ID 같은 높은 카디널리티 컬럼을 다룰 때는 실제 값으로 작업하는 대신 근사 분석을 위해 스케치(sketches) 사용을 고려해 보세요. 스케치로 데이터를 수집하면 Druid는 원본 원시 데이터를 저장하지 않고, 나중에 쿼리 시점의 계산에 넣을 수 있는 "스케치"를 저장합니다. 스케치의 인기 사용 사례는 카운트-디스틴트(count-distinct)와 분위수 계산이에요. 각 스케치는 딱 한 종류의 계산을 위해 설계됩니다.

일반적으로 스케치 사용은 두 가지 주요 목적을 지녀요: 롤업 개선과 쿼리 시점 메모리 공간(footprint) 감소입니다.

스케치는 여러 개의 고유 값을 같은 스케치로 접을 수 있게 해서 롤업 비율을 개선해요. 예를 들어 사용자 ID만 빼고 완전히 동일한 두 행이 있다면(아마 두 사용자가 같은 시간에 같은 동작을 했을 때), 그것을 있는 그대로 저장하는 대신 카운트-디스틴트 스케치에 저장하면 두 행 대신 한 행으로 저장할 수 있습니다. 사용자 ID를 검색하거나 정확한 고유 카운트를 계산할 수는 없게 되지만, 근사 고유 카운트는 여전히 계산할 수 있고 저장 공간도 줄어듭니다.

스케치는 서버 사이에 셔플해야 하는 데이터 양을 제한해서 쿼리 시점 메모리 공간을 줄여 줍니다. 예를 들어 분위수 계산에서 모든 데이터 포인트를 중앙 위치로 보내서 정렬하고 분위수를 계산할 필요 없이, Druid는 포인트의 스케치만 보내면 됩니다. 이렇게 하면 데이터 전송 요구량을 수 킬로바이트로 줄일 수 있어요.

Druid에서 사용 가능한 스케치에 대한 자세한 내용은 approximate aggregators 페이지를 참고하세요.

동영상을 선호한다면, Druid에서 스케치에 관한 컨퍼런스 강연인 Not exactly!를 확인해 보세요.

문자열 vs 숫자 dimension

컬럼을 숫자 타입 dimension(Long, Double 또는 Float)으로 수집하고 싶다면 dimensionsSpec의 dimensions 섹션에서 컬럼 타입을 지정해야 해요. 타입을 생략하면 Druid는 컬럼을 기본 String 타입으로 수집합니다.

문자열과 숫자 컬럼 사이에는 성능 트레이드오프가 있어요. 숫자 컬럼은 일반적으로 문자열 컬럼보다 그룹화가 더 빠릅니다. 하지만 문자열 컬럼과 달리 숫자 컬럼에는 인덱스가 없어서 필터링이 더 느릴 수 있습니다. 자신의 사용 사례에 최적인 선택을 찾으려면 실험해 보는 게 좋아요.

숫자 dimension 구성 방법에 대한 자세한 내용은 dimensionsSpec 문서를 참고하세요.

보조 타임스탬프 (Secondary timestamps)

Druid 스키마에는 항상 기본 타임스탬프(primary timestamp)가 포함돼야 해요. 기본 타임스탬프는 데이터 파티셔닝·정렬에 사용되므로, 가장 자주 필터링하게 될 타임스탬프여야 합니다. Druid는 기본 타임스탬프 컬럼의 시간 범위에 해당하는 데이터를 빠르게 식별·검색할 수 있습니다.

데이터에 타임스탬프가 둘 이상 있다면 나머지를 보조 타임스탬프로 수집할 수 있어요. 가장 좋은 방법은 밀리초 단위의 long 타입 dimension으로 수집하는 것입니다. 필요하다면 transformSpec과 timestamp_parse 같은 표현식을 사용해서 이 형식으로 만들 수 있는데, timestamp_parse는 밀리초 타임스탬프를 반환합니다.

쿼리 시점에는 MILLIS_TO_TIMESTAMP, TIME_FLOOR 같은 SQL 시간 함수로 보조 타임스탬프를 쿼리할 수 있어요. 네이티브 Druid 쿼리를 사용한다면 표현식(expressions)을 사용할 수 있습니다.

중첩 dimension (Nested dimensions)

중첩 데이터를 COMPLEX<json> 데이터 타입의 Druid 컬럼으로 수집·저장할 수 있어요. 자세한 내용은 Nested columns를 참고하세요.

중첩 컬럼 기능이 지원하지 않는 형식의 중첩 데이터를 수집하고 싶다면 flattenSpec 객체로 평탄화해야 해요. 예를 들어 다음 형태의 데이터가 있다면:

{ "foo": { "bar": 3 } }

인덱싱 전에 다음과 같이 변환해야 합니다.

{ "foo_bar": 3 }

자세한 내용은 flattenSpec 문서를 참고하세요.

수집된 이벤트 수 세기

롤업이 활성화되면 쿼리 시점의 count 애그리게이터는 실제로 수집된 행 수를 알려 주지 않아요. 그것들은 Druid datasource의 행 수를 알려 주며, 이는 수집된 행 수보다 작을 수 있습니다.

이 경우 수집 시점의 count 애그리게이터로 이벤트 수를 셀 수 있어요. 다만 이 metric을 쿼리할 때는 longSum 애그리게이터를 사용해야 한다는 점을 기억하세요. 쿼리 시점의 count 애그리게이터는 시간 interval에 대한 Druid 행 수를 반환하며, 이것으로 롤업 비율을 결정할 수 있습니다.

예시로 명확히 하면, 수집 스펙이 다음을 포함한다면:

"metricsSpec": [    { "type": "count", "name": "count" }]

수집된 행 수는 다음과 같이 쿼리해야 해요.

"aggregations": [    { "type": "longSum", "name": "numIngestedEvents", "fieldName": "count" }]

dimension의 스키마 자동 발견

Druid는 다음과 같은 두 가지 방법 중 하나로 데이터의 스키마를 추론할 수 있어요.

  • 타입 인식 스키마 발견(Type-aware schema discovery): Druid가 데이터의 스키마와 타입을 추론. 타입 인식 스키마 발견은 네이티브 배치 및 스트리밍 수집에서 사용 가능.
  • 문자열 기반 스키마 발견(String-based schema discovery): 발견된 모든 컬럼이 네이티브 문자열 또는 멀티-값 문자열 컬럼으로 타입이 지정.

타입 인식 스키마 발견

정보

타입 인식 스키마 발견을 사용하면 ARRAY 타입 컬럼을 다루는 방식에 따라 다운스트림 BI 도구에 영향을 줄 수 있음에 유의하세요.

dimensionsSpec.useSchemaDiscovery를 true로 설정하고 dimensions 목록에 일부 또는 아무것도 정의하지 않으면 Druid가 데이터의 스키마와 타입을 부분적 혹은 완전히 추론하게 할 수 있어요.

타입 인식 스키마 발견을 수행할 때 Druid는 입력 데이터의 모든 컬럼(제외 목록에 없는 것들)을 발견할 수 있습니다. Druid는 STRING, LONG, DOUBLE, ARRAY<STRING>, ARRAY<LONG>, ARRAY<DOUBLE>, 또는 중첩 데이터용 COMPLEX<json> 중 가장 적절한 네이티브 Druid 타입을 자동으로 선택합니다. 네이티브 불리언 타입을 가진 input format의 경우 Druid는 그 값을 long으로 수집해요. 배열 타입 컬럼은 array 함수나 UNNEST로 쿼리할 수 있고, 중첩 컬럼은 JSON 함수로 쿼리할 수 있습니다.

혼합 타입 컬럼은 세그먼트 사이 스키마 차이에 대해 같은 규칙을 따르며, 컬럼의 모든 값을 표현할 수 있는 가장 덜 제한적인 타입으로 나타납니다. 예를 들어:

  • 혼합 숫자 컬럼은 DOUBLE
  • 문자열이 하나라도 있으면 그 컬럼은 STRING
  • 배열이 있으면 컬럼은 가장 덜 제한적인 요소 타입의 배열이 됨
  • 중첩 데이터나 중첩 데이터의 배열은 COMPLEX<json> 중첩 컬럼이 됨

혼합 타입 값의 그룹화·필터링·집계는 모든 값이 가장 덜 제한적인 타입으로 표현된 것처럼 이 컬럼들을 처리합니다. 예외는 scan 쿼리인데, 값들을 원래 혼합 타입으로 반환하지만 값에 대한 다운스트림 작업은 여전히 공통 타입으로 강제 변환합니다.

이미 문자열 기반 스키마 발견을 사용 중이고 마이그레이션하고 싶다면 Migrating to type-aware schema discovery를 참고하세요.

문자열 기반 스키마 발견

dimensionsSpec.useSchemaDiscovery를 true로 설정하지 않아도, 다음 조건 중 하나라도 충족되면 Druid는 수집에 문자열 기반 스키마 발견을 사용할 수 있어요.

  • dimension 목록이 비어 있음
  • includeAllDimensions를 true로 설정

Druid는 기본 타입과 기본 타입의 배열을 네이티브 Druid 문자열 타입으로 강제 변환합니다. 중첩 데이터 구조와 중첩 데이터 구조의 배열은 무시되고 수집되지 않아요.

타입 인식 스키마 발견으로 마이그레이션

이전에 문자열 기반 스키마 발견을 사용했고 타입 인식 스키마 발견으로 마이그레이션하고 싶다면 다음을 하세요.

  • 멀티-값 dimension(MVD)을 사용하는 쿼리를, MVD 동작에 의존하지 않도록 UNNEST를 다른 함수와 함께 사용하도록 업데이트하세요. 타입 인식 스키마 발견은 MVD 대신 ARRAY 타입 컬럼을 생성하므로, MVD 기능을 사용하는 쿼리는 실패할 것입니다.
  • 혼합 타입 입력을 인지하고 타입 인식 스키마 발견이 그것을 어떻게 처리하는지 테스트하세요. Druid는 그것들을 가장 덜 제한적인 타입으로 캐스팅하려고 시도합니다.
  • 숫자 타입에 문제가 발견되면 명시적으로 캐스팅해야 할 수도 있어요. 일반적으로 Druid가 강제 변환을 처리해 줍니다.
  • 계속 제외하고 싶다면 dimension 제외 목록을 업데이트하고 중첩 컬럼을 추가하세요. 문자열 기반 스키마 발견은 중첩 컬럼을 자동으로 무시하지만, 타입 인식 스키마 발견은 그것들을 수집합니다.

같은 컬럼을 dimension과 metric으로 둘 다 포함하기

고유 ID를 다루는 한 가지 워크플로는 특정 ID로 필터링하면서, 동시에 ID 컬럼의 빠른 고유 카운트를 수행하는 것입니다. 스키마-리스(schema-less) dimension을 사용하지 않는다면, metric의 name을 dimension과 다르게 설정해서 이 사용 사례를 지원할 수 있어요. 스키마-리스 dimension을 사용한다면 모범 사례는 같은 컬럼을 두 번 포함하는 것입니다. 한 번은 dimension으로, 한 번은 hyperUnique metric으로요. ETL 시점에 약간의 작업이 필요할 수 있습니다.

예를 들어 스키마-리스 dimension의 경우 같은 컬럼을 반복하세요:

{ "device_id_dim": 123, "device_id_met": 123 }

그리고 metricsSpec에 다음을 포함합니다:

{ "type": "hyperUnique", "name": "devices", "fieldName": "device_id_met" }

device_id_dim은 자동으로 dimension으로 잡힐 것입니다.

더 알아보기 (Learn more)