차원 테이블

차원 테이블 (Dimension Table)

작은 참조 데이터를 쿼리 시점에 팩트 데이터와 조인 방식으로 보강할 때 쓰는 특수한 오프라인 테이블인 차원 테이블을 설명하는 페이지예요.

출처: Dimension Table

본문

차원 테이블(Dimension Table)은 쿼리 시점에 팩트 데이터를 조인 방식으로 보강(enrichment)하기 위해 설계된 특별한 오프라인 테이블이에요. 단일 스테이지 엔진의 lookup UDF 또는 멀티 스테이지 엔진의 lookup join 전략과 함께 사용해 참조 데이터로 쿼리 결과를 장식해요.

차원 테이블을 언제 사용할까

쿼리 시점에 작고 비교적 정적인 참조 데이터셋의 속성으로 큰 팩트 테이블을 보강해야 할 때 차원 테이블을 사용해요. 일반적인 예로는:

  • 팀 ID에서 사람이 읽을 수 있는 팀 이름 조회.
  • 클릭스트림 이벤트를 제품 카탈로그 속성으로 보강.
  • 거래 레코드에 고객 또는 상점 메타데이터 장식.

다음 중 하나라도 해당된다면 일반 오프라인 또는 실시간 테이블이 더 적합해요:

  • 참조 데이터가 크다 (수억 행 또는 수 기가바이트).
  • 데이터가 자주 바뀌어 실시간 수집이 필요하다.
  • 시간 기반 파티셔닝, 보존 정책, 또는 하이브리드 테이블 설정이 필요하다.
  • 참조 데이터를 복잡한 집계로 독립적으로 쿼리해야 한다.

차원 테이블 동작 방식

테이블이 차원 테이블로 표시되면 Pinot은 그 세그먼트 전체를 테넌트의 모든 서버에 복제해요. 각 서버에서 데이터는 테이블의 기본 키로 키가 매겨진 인메모리 해시 맵에 로드되어, 쿼리 실행 중 상수 시간 조회를 가능하게 해요.

데이터가 완전히 복제되고 메모리에 유지되므로, 차원 테이블은 각 서버의 힙에 편안히 들어갈 만큼 작아야 해요. 대규모 데이터셋을 위한 것은 아니에요.

메모리 로딩 모드

Pinot은 dimensionTableConfig의 disablePreload 설정으로 제어되는 두 가지 로딩 모드를 지원해요:

모드 (Mode) disablePreload 메모리 사용 (Memory usage) 조회 속도 (Lookup speed) 설명 (Description)
빠른 조회 (Fast lookup) (기본) false 높음 빠름 모든 행이 인메모리 해시 맵(Object[] -> Object[])으로 완전히 구체화돼요. 모든 컬럼 값이 상수 시간 검색을 위해 맵에 저장돼요.
메모리 최적화 (Memory-optimized) true 낮음 약간 느림 해시 맵에는 기본 키와 세그먼트/docId 참조만 저장돼요. 컬럼 값은 각 조회 시 세그먼트에서 읽어요. 이는 조회 속도를 낮은 힙 사용과 맞바꿔요.

차원 테이블이 상대적으로 크고 힙 압력을 줄이고 싶다면 메모리 최적화 모드를 선택하세요 (약간 느린 조회 비용).

크기 제한과 메모리 고려 사항

  • 클러스터 수준 최대 크기: 컨트롤러 설정 속성 controller.dimTable.maxSize는 단일 차원 테이블에 허용되는 최대 스토리지 쿼터를 설정해요. 기본값은 200 MB예요. 요청된 quota.storage가 이 한도를 초과하면 테이블 생성이 실패해요.
  • 힙 영향: 빠른 조회 모드에서 전체 테이블이 모든 서버의 Java 힙에 구체화돼요. 디스크에서 100 MB인 테이블도 역직렬화 후 훨씬 더 많은 메모리를 소비할 수 있어요. 차원 테이블을 추가하거나 늘릴 때 서버 힙 사용을 모니터링하세요.
  • 복제 오버헤드: 테넌트의 모든 서버가 전체 복사본을 보유하므로, 차원 테이블을 추가하면 그 메모리 풋프린트가 서버 수만큼 곱해져요.

{% hint style="warning" %} 지침으로서, 차원 테이블은 수십만 행 미만, 그리고 controller.dimTable.maxSize 한도에 크게 못 미치는 수준으로 유지하세요. 가용 힙에 근접하거나 초과하는 테이블은 서버에서 아웃 오브 메모리 오류를 일으킵니다. {% endhint %}

설정 (Configuration)

테이블 설정

테이블 설정에서 다음 속성을 지정해 테이블을 차원 테이블로 표시해요:

속성 (Property) 필수 (Required) 설명 (Description)
isDimTable 예 true로 설정해 테이블을 차원 테이블로 지정
ingestionConfig.batchIngestionConfig.segmentIngestionType 예 REFRESH로 설정해야 함. 차원 테이블은 append 대신 세그먼트 교체를 사용해 인메모리 해시 맵이 최신 데이터로 재구축되게 함
segmentAssignmentConfigMap.OFFLINE.segmentAssignmentStrategy 아니오 설정한다면 allservers여야 함. 설정하지 않아도 Pinot이 차원 테이블에 자동으로 all-servers 배정을 사용하므로 동작함
quota.storage 권장 테이블 스토리지 쿼터. 클러스터 수준 controller.dimTable.maxSize(기본 200 MB)를 초과하면 안 됨
dimensionTableConfig.disablePreload 아니오 true로 설정해 메모리 최적화 모드 사용(전체 행 대신 기본 키와 세그먼트 참조만 저장). 기본값 false(빠른 조회)
dimensionTableConfig.errorOnDuplicatePrimaryKey 아니오 true로 설정해 세그먼트 간 중복 기본 키가 감지되면 세그먼트 로딩 실패. 기본값 false(마지막에 로드된 세그먼트가 우선)

스키마 설정

차원 테이블 스키마는 metricFieldSpecs 대신 dimensionFieldSpecs를 사용해요. primaryKeyColumns 배열이 필수이며, 조회에 사용되는 키를 정의해요.

{% hint style="warning" %} 차원 테이블은 항상 allservers 세그먼트 배정 전략을 사용해 모든 세그먼트가 테넌트의 모든 서버에 복제됩니다. 전략을 명시적으로 설정한다면 segmentAssignmentConfigMap.OFFLINE.segmentAssignmentStrategy 아래에서 하세요. 차원 테이블에 balanced, replica-group, round-robin 세그먼트 배정을 설정하지 마세요. Pinot은 테이블 검증 중 이 값들을 거부합니다. {% endhint %}

예시 테이블 설정

{
  "OFFLINE": {
    "tableName": "dimBaseballTeams_OFFLINE",
    "tableType": "OFFLINE",
    "segmentsConfig": {
    },
    "segmentAssignmentConfigMap": {
      "OFFLINE": {
        "segmentAssignmentStrategy": "allservers"
      }
    },
    "ingestionConfig": {
      "batchIngestionConfig": {
        "segmentIngestionType": "REFRESH"
      }
    },
    "quota": {
      "storage": "200M"
    },
    "isDimTable": true,
    "dimensionTableConfig": {
      "disablePreload": false,
      "errorOnDuplicatePrimaryKey": false
    }
  }
}

예시 스키마 설정

{
  "schemaName": "dimBaseballTeams",
  "primaryKeyColumns": ["teamID"],
  "dimensionFieldSpecs": [
    {
      "dataType": "STRING",
      "name": "teamID"
    },
    {
      "dataType": "STRING",
      "name": "teamName"
    },
    {
      "dataType": "STRING",
      "name": "teamAddress"
    }
  ]
}

LOOKUP 함수로 쿼리하기

차원 테이블을 사용하는 주요 방법은 단일 스테이지 쿼리 엔진의 LOOKUP UDF를 통한 것이에요. 이 함수는 차원 테이블에 대한 기본 키 조회를 수행하고 컬럼 값을 반환해요.

문법 (Syntax)

LOOKUP('dimTable', 'dimColToLookUp', 'dimJoinKey1', factJoinKey1 [, 'dimJoinKey2', factJoinKey2 ]*)
  • dimTable -- 차원 테이블 이름 (문자열 리터럴).
  • dimColToLookUp -- 차원 테이블에서 검색할 컬럼 (문자열 리터럴).
  • dimJoinKey / factJoinKey -- 조인 키 쌍: 차원 테이블 컬럼 이름(문자열 리터럴)과 그에 대응하는 팩트 테이블 컬럼 표현식.

단일 키 조회

SELECT
  playerName,
  teamID,
  LOOKUP('dimBaseballTeams', 'teamName', 'teamID', teamID) AS teamName,
  LOOKUP('dimBaseballTeams', 'teamAddress', 'teamID', teamID) AS teamAddress
FROM baseballStats
LIMIT 10

복합 키 조회

차원 테이블이 복합 기본 키를 가질 때 스키마의 primaryKeyColumns와 같은 순서로 여러 키 쌍을 제공해요:

SELECT
  customerId,
  LOOKUP('billing', 'city', 'customerId', customerId, 'creditHistory', creditHistory) AS city
FROM transactions
LIMIT 10

멀티 스테이지 엔진

멀티 스테이지 엔진(MSE)에서는 LOOKUP UDF 대신 lookup join 전략 힌트와 함께 표준 JOIN을 사용해요:

SELECT /*+ lookupJoinStrategy(dim_billing) */
  t.customerId,
  b.city
FROM transactions t
JOIN billing b
  ON t.customerId = b.customerId
LIMIT 10

자세한 내용은 lookup join 전략 참고.

리프레시와 업데이트 전략

차원 테이블은 segmentIngestionType: REFRESH를 사용하므로, 새 세그먼트를 업로드하면 기존 세그먼트를 교체하고 모든 서버에서 인메모리 해시 맵의 전체 리로드를 트리거해요. 증분 업데이트 메커니즘은 없어요.

일반적인 리프레시 패턴:

  • 스케줄된 배치 잡: 진실 원본에서 세그먼트를 재구축해 Pinot에 업로드하는 주기적 수집 잡(예: daily 또는 hourly) 실행.
  • 온디맨드 리프레시: 참조 데이터가 바뀔 때마다 Pinot REST API로 세그먼트 업로드 트리거.

{% hint style="info" %} 리프레시 중에는 새 맵이 완전히 로드될 때까지 기존 해시 맵이 조회에 활성 상태로 남아 있습니다. 리프레시 중 쿼리 다운타임은 없지만, 기존 데이터가 서빙되는 짧은 기간이 있습니다. {% endhint %}

중복 기본 키 처리

여러 세그먼트가 같은 기본 키를 포함할 때 기본 동작은 마지막에 로드된 세그먼트가 우선하는 것(세그먼트는 생성 시간순 정렬)이에요. dimensionTableConfig에서 errorOnDuplicatePrimaryKey: true로 설정하면 중복 감지 시 빠르게 실패해요. REFRESH 수집에서는 보통 세그먼트가 하나뿐이므로 세그먼트 간 중복은 흔하지 않아요.

성능 모범 사례

  • 테이블을 작게 유지. 차원 테이블은 모든 서버의 메모리에 완전히 로드돼요. 수천 개에서 수십만 개 이하의 행을 목표로 하세요.
  • 좁은 스키마 사용. 조회에 필요한 컬럼만 포함해 메모리 소비를 줄이세요.
  • 올바른 로딩 모드 선택. 최상의 쿼리 성능을 위해 빠른 조회(기본) 사용. 힙 사용이 우려될 때만 메모리 최적화 모드(disablePreload: true)로 전환.
  • 스토리지 쿼터 설정. 항상 quota.storage를 설정해 실수로 과대한 데이터 업로드를 방지.
  • 리프레시 빈도 최소화. 각 리프레시는 해시 맵의 전체 리로드를 트리거해요. 필요한 것보다 더 자주 리프레시하지 마세요.
  • 서버 힙 모니터링. 차원 테이블 추가 후 서버 JVM 힙 메트릭을 확인해 충분한 여유가 있는지 확인.

제한 사항 (Limitations)

  • 오프라인 전용. 차원 테이블은 오프라인 테이블이어야 해요. 실시간 또는 하이브리드 테이블은 될 수 없어요.
  • 전체 복제. 모든 세그먼트가 테넌트의 모든 서버에 복제되므로 메모리 사용은 서버 수에 비례해 늘어나요.
  • 증분 업데이트 없음. 각 리프레시마다 전체 세그먼트가 교체되어야 해요; 행 수준 업데이트는 지원되지 않아요.
  • 기본 키 필수. 스키마가 primaryKeyColumns를 정의해야 해요. 기본 키 없는 조회는 지원되지 않아요.
  • 단일 스테이지 LOOKUP UDF 제한. LOOKUP 함수의 차원 테이블 컬럼 참조는 컬럼 식별자가 아니라 문자열 리터럴이어야 해요. 쿼리의 FROM 절에 없는 테이블을 참조하기 때문이에요.
  • 시간 기반 파티셔닝/보존 없음. 차원 테이블은 세그먼트 보존 정책이나 시간 기반 파티셔닝을 지원하지 않아요.

더 알아보기 (Learn more)