사전 인덱스
사전 인덱스 (Dictionary Index)
방대한 데이터셋을 다룰 때 값이 여러 번 반복되는 것이 흔해요. 저장 효율성을 높이고 쿼리 지연을 줄이기 위해 반복 데이터에는 사전 인덱스(dictionary index)를 사용할 것을 강력히 권장해요. 이것이 Pinot가 사전 인코딩을 기본으로 활성화하는 이유예요. 다만 높은 카디널리티 컬럼에서는 비활성화하는 것이 좋아요.
출처: 문서
본문
언제 사용하나 (When to use)
카디널리티가 낮거나 중간인 컬럼(즉 고유 값 수가 총 행 수보다 훨씬 적은 경우)에는 사전 인코딩(기본값)을 활성화하세요. 사전 인코딩은 UUID나 자유 텍스트 필드 같은 높은 카디널리티 컬럼에는 덜 유용해요. 그러한 경우 사전을 비활성화하고 raw 인코딩을 사용하면 공간을 절약하고 오버헤드를 줄일 수 있어요.
지원 컬럼 유형
사전 인코딩은 모든 데이터 유형에서 지원돼요: INT, LONG, FLOAT, DOUBLE, BIG_DECIMAL, STRING, BYTES, UUID, BOOLEAN, TIMESTAMP, JSON.
가변 길이 유형(STRING, BYTES, BIG_DECIMAL)의 경우 값의 길이가 크게 다를 때 useVarLengthDictionary 활성화를 고려하세요.
다른 인덱스에 미치는 영향
Pinot에서 사전은 인덱스이자 실제 인코딩 역할을 해요. 따라서 사전이 활성화되면 다른 일부 인덱스의 동작이나 레이아웃이 수정돼요. 사전과 다른 인덱스의 관계는 다음 표에 요약되어 있어요.
| Index | Conditional | Description |
|---|---|---|
| forward | 구현이 사전 활성화 여부에 따라 달라짐. | |
| range | 사전이 활성화되면 사전 ID를 사용하고, 사전 없는 숫자 RAW 컬럼에는 raw 값을 사용함. RAW + 사전 범위 인덱스는 반드시 range index version 2를 사용해야 함. | |
| inverted | 사전 ID를 사용함. 레거시 구성으로 사전이 비활성화되지 않았다면 RAW 컬럼에 대해 독립형 사전을 구체화할 수 있음. | |
| json | optimizeDictionary일 때 |
사전 비활성화. |
| text | optimizeDictionary일 때 |
사전 비활성화. |
| FST and IFST | 사전 값을 사용함. 레거시 구성으로 사전이 비활성화되지 않았다면 RAW STRING 컬럼에 대해 독립형 사전을 구체화할 수 있음. | |
| H3 (or geospatial) | 사전과 호환되지 않음. |
구성 (Configuration)
결정적으로 사전 활성화/비활성화
많은 다른 인덱스와 달리 사전 인덱스는 고유 값 수가 행 수보다 크게 낮을 것이라는 가정 아래 기본적으로 활성화되어 있어요.
이 가정이 성립하지 않으면 indexes.dictionary 내에서 disabled 속성을 true로 설정해 특정 컬럼의 사전을 비활성화할 수 있어요.
{
"fieldConfigList": [
{
"name": "col1",
"indexes": {
"dictionary": {
"disabled": true
}
}
},
...
],
...
}
대안으로 encodingType 속성을 변경할 수 있어요. 예를 들어:
{
"fieldConfigList": [
{
"name": "col1",
"encodingType": "RAW"
},
...
],
...
}
원하는 옵션을 선택할 수 있지만 일관성을 유지하는 것이 중요해요. Pinot는 같은 컬럼과 인덱스가 다른 위치에 정의된 테이블 구성을 거부하기 때문이에요.
컬럼이 RAW forward index를 유지하더라도, 다른 활성화된 인덱스가 사전 ID나 사전 값을 필요로 할 때 Pinot는 독립형 사전을 여전히 구체화할 수 있어요. 이를 통해 RAW 컬럼이 forward-index 인코딩을 바꾸지 않고도 bitmap inverted index나 FST/IFST 같은 기능을 지원할 수 있어요.
테이블 구성 업데이트에는 fieldConfigList의 명시적 dictionary 항목을 선호하세요.
{
"fieldConfigList": [
{
"name": "myColumn",
"encodingType": "RAW",
"indexes": {
"dictionary": {},
"inverted": {}
}
}
]
}
필드 수준 encodingType: RAW 구성은 같은 FieldConfig에 구성된 인덱스가 사전을 필요로 할 때 사전을 활성 상태로 유지할 수도 있어요. 레거시 no-dictionary 설정은 다르거나: tableIndexConfig.noDictionaryColumns와 tableIndexConfig.noDictionaryConfig는 보조 인덱스 검증이 실행되기 전에 사전을 여전히 비활성화해요.
같은 컬럼에 사전 기반 보조 인덱스가 활성화되면 다음 레거시 형태는 거부돼요.
{
"tableIndexConfig": {
"noDictionaryColumns": ["myColumn"],
"invertedIndexColumns": ["myColumn"]
}
}
해당 테이블을 마이그레이션하려면 레거시 no-dictionary 구성에서 컬럼을 제거하고, encodingType: RAW를 유지하며, fieldConfigList에 indexes.dictionary와 보조 인덱스를 추가하세요.
휴리스틱하게 사전 활성화
대부분의 경우 테이블을 만드는 도메인 전문가는 사전이 유용할지 아닐지 알고 있어요. 예를 들어 랜덤 값이나 공용 IP를 가진 컬럼은 카디널리티가 클 것이므로 즉시 raw 인코딩 대상이 될 수 있고, 직원 ID 같은 컬럼은 카디널리티가 작아 좋은 사전 후보로 쉽게 인식될 수 있어요. 하지만 때로는 결정이 명확하지 않을 수 있어요. 이런 경우를 위해 Pinot는 실제 값과 관계 요인에 따라 사전을 휴리스틱하게 생성하도록 구성될 수 있어요.
이 휴리스틱이 활성화되면 Pinot는 각 후보 컬럼에 대해 절약 요인(saving factor)을 계산해요. 이 요인은 raw로 인코딩된 forward index 크기와 사전으로 인코딩된 동일 인덱스 크기의 비율이에요. 후보 컬럼의 절약 요인이 절약 비율보다 작으면 사전이 생성되지 않아요.
휴리스틱의 후보로 간주되려면 컬럼은 다음 조건을 만족해야 해요.
- 사전 인코딩으로 표시됨(raw로 표시된 컬럼은 항상 raw로 인코딩됨).
- 단일 값이어야 함(다중 값 컬럼은 휴리스틱에서 결코 고려되지 않음).
- int, long, double, timestamp 등 고정 크기 유형이어야 함. json, strings, bytes 같은 가변 크기 유형은 휴리스틱에서 결코 고려되지 않음.
- text index나 JSON index로 인덱싱되지 않아야 함(카디널리티가 매우 클 때만 유용하기 때문).
선택적으로 이 기능을 메트릭 컬럼에만 적용하고 차원 컬럼은 건너뛸 수 있어요.
이 기능은 테이블 구성의 indexingConfig 객체 내에서 활성화될 수 있어요. 이 휴리스틱을 지배하는 파라미터는 다음과 같아요.
| Parameter | Default | Description |
|---|---|---|
| optimizeDictionary | false | 모든 컬럼에 대한 휴리스틱을 활성화하고 몇 가지 추가 규칙을 활성화함. |
| optimizeDictionaryForMetrics | false | 메트릭 컬럼에 대한 휴리스틱을 활성화함. |
| optimizeDictionaryType | false | 가변 폭 컬럼에 대해 세그먼트의 모든 값이 같은 길이일 때 자동으로 고정 폭 사전을 선택함. |
| noDictionarySizeRatioThreshold | 0.85 | 휴리스틱에 사용되는 절약 비율. |
중요한 점:
- 이 파라미터들은 테이블 내의 모든 컬럼에 대해 구성돼요.
optimizeDictionary는optimizeDictionaryForMetrics보다 우선해요.optimizeDictionaryType은 사전을 비활성화하지 않아요. 가변 폭 컬럼의 사전 값을 고정 폭으로 저장할지 가변 폭으로 저장할지만 결정해요.
파라미터
사전은 다음 옵션으로 구성될 수 있어요.
| Parameter | Default | Description |
|---|---|---|
| onHeap | false | 인덱스를 힙에 로드할지 off-heap에 로드할지. |
| useVarLengthDictionary | false | 가변 길이 값을 저장하는 방법 결정. |
| intern | empty object | 인턴(interning) 구성. on-heap 사전 전용. 아래에서 자세히 설명. |
| intern.capacity | null | 인턴해야 할 값의 수. |
가변 길이 사전
useVarLengthDictionary 파라미터는 차지하는 바이트 수가 다양한 값이 있는 컬럼에만 영향을 줘요. 여기에는 문자열, 바이트, big decimal 등 가변 바이트 수가 필요한 컬럼 유형과 세그먼트 내 모든 값이 같은 바이트 수를 차지하지 않는 시나리오가 포함돼요. 예를 들어 문자열이 일반적으로 가변 바이트 수로 저장되어도, 세그먼트에 "a", "b", "c" 값만 있으면 Pinot는 세그먼트의 모든 값이 같은 바이트 수로 표현될 수 있음을 식별해요.
기본적으로 useVarLengthDictionary는 false로 설정되어 있으며, Pinot는 세그먼트 내 가장 큰 값의 길이를 계산해요. 이 길이는 모든 값에 사용돼요. 이 접근 방식은 모든 값이 효율적으로 저장될 수 있도록 보장하며, 값의 길이가 비슷할 때 더 빠른 접근과 더 압축된 레이아웃을 제공해요.
데이터셋에 매우 큰 값이 몇 개와 매우 작은 값이 다수 포함되어 있다면 useVarLengthDictionary를 true로 설정해 가변 길이 인코딩을 사용하도록 지시하는 것이 좋아요. 가변 인코딩이 사용되면 Pinot는 각 항목의 길이를 저장해야 해요. 결과적으로 항목 저장 비용은 실제 크기에 오프셋 4바이트가 추가된 값이 돼요.
On-heap 사전
사전 데이터는 항상 off-heap에 저장돼요. 일반적으로 사전을 그렇게 유지하는 것이 권장돼요. 그러나 카디널리티가 작고 on-heap 메모리 사용이 허용 가능한 경우 onHeap 파라미터를 true로 설정해 메모리로 복사할 수 있어요.
기억하세요: On-heap 사전은 권장되지 않아요.
On-heap 사전은 지연을 약간 줄일 수 있지만 Pinot가 사용하는 힙 메모리를 크게 증가시키고 가비지 컬렉션 시간을 증가시켜 out of memory 문제를 일으킬 수 있어요.
off-heap 사전이 사용되면 데이터는 접근할 때마다 역직렬화돼요. 기본 유형(int나 long)에는 문제가 없지만 복잡한 유형(문자열이나 바이트)의 경우 데이터가 접근할 때마다 역직렬화된다는 뜻이에요. On-heap 사전은 데이터를 역직렬화된 형식으로 메모리에 유지해 쿼리 시간에 할당이 필요 없도록 이 문제를 해결해요.
그러나 on-heap 사전은 메모리 사용의 비용이 있고 그 비용은 동시에 접근되는 세그먼트 수에 비례해요. 다른 모든 인덱스와 마찬가지로 사전 범위가 세그먼트로 제한된다는 점을 유의하는 것이 중요해요. 즉 1,000개 세그먼트가 있는 테이블과 한 컬럼의 사전이 있으면 메모리에 1,000개의 사전이 있을 수 있어요. 고유 값이 세그먼트에 걸쳐 반복되는 경우 이는 메모리 낭비가 될 수 있어요. 이 문제를 해결하기 위해 Pinot는 사전 값의 캐시를 유지하고 세그먼트에 걸쳐 재사용할 수 있어요. 이 캐시는 서로 다른 테이블이나 컬럼 간에 공유되지 않으며 최대 크기는 dictionary.intern.capacity 옵션으로 제어돼요.
문자열과 바이트 컬럼만 인턴될 수 있어요. Pinot는 다른 데이터 유형의 컬럼에 사용될 때 intern 구성을 무시해요.
on-heap 사전을 intern 모드로 구성하는 예시:
{
"tableName": "somePinotTable",
"fieldConfigList": [
{
"name": "strColumn",
"encodingType": "DICTIONARY",
"indexes": {
"dictionary": {
"onHeap": true,
"intern": {
"capacity":32000
}
}
}
}
]
}