Map(K, V)
데이터 타입 Map(K, V)은 key-value 쌍을 저장합니다.
다른 데이터베이스와 달리 ClickHouse에서 맵은 유일(unique)하지 않습니다. 즉, 하나의 맵에 동일한 키를 가진 두 개의 요소가 포함될 수 있습니다.
(이는 맵이 내부적으로 Array(Tuple(K, V))로 구현되어 있기 때문입니다.)
맵 m에서 키 k에 대한 값을 얻기 위해 m[k] 구문을 사용할 수 있습니다.
또한 m[k]는 맵 전체를 스캔하므로, 연산 시간은 맵 크기에 비례하여 선형적으로 증가합니다.
파라미터
K— 맵 키(Map keys)의 타입입니다. Nullable 타입 및 Nullable 타입과 중첩된 LowCardinality 타입을 제외한 임의의 타입입니다.V— 맵 값(Map values)의 타입입니다. 임의의 타입입니다.
예시
맵 타입 컬럼을 가진 테이블을 생성합니다:
key2 값을 선택하려면:
결과:
요청한 키 k가 맵에 포함되어 있지 않으면 m[k]는 값 타입의 기본값을 반환합니다. 예를 들어 정수 타입은 0, 문자열 타입은 ''을(를) 반환합니다.
맵에 키가 존재하는지 확인하려면 mapContains 함수를 사용할 수 있습니다.
결과:
Tuple을 맵(Map)으로 변환하기
Tuple() 타입의 값은 함수 CAST를 사용하여 Map() 타입의 값으로 형변환할 수 있습니다.
예시
쿼리:
결과:
맵 서브컬럼 읽기
전체 맵을 모두 읽지 않도록, 상황에 따라 서브컬럼 keys와 values를 사용할 수 있습니다.
예시
쿼리:
결과:
MergeTree의 버킷형 맵 직렬화
기본적으로 MergeTree의 Map 컬럼은 단일 Array(Tuple(K, V)) 스트림으로 저장됩니다.
m['key']로 단일 키를 읽으려면 전체 컬럼을 스캔해야 합니다. 즉, 키 하나만 필요하더라도 모든 행의 모든 key-value 쌍을 검사해야 합니다.
서로 다른 키가 많은 맵에서는 이것이 병목이 됩니다.
버킷 기반 직렬화(with_buckets)는 키를 해시하여 key-value 쌍을 여러 개의 독립적인 하위 스트림(버킷)으로 분할합니다.
쿼리에서 m['key']에 접근하면 해당 키가 들어 있는 버킷만 디스크에서 읽고, 다른 모든 버킷은 건너뜁니다.
버킷 기반 직렬화 활성화
삽입 속도 저하를 방지하려면 제로 레벨 파트(INSERT 시 생성됨)에는 basic 직렬화를 유지하고, 머지된 파트에만 with_buckets를 사용할 수 있습니다:
작동 방식
데이터 파트가 with_buckets 직렬화로 쓰이면 다음과 같이 작동합니다.
- 행당 평균 키 수를 블록 통계에서 계산합니다.
- 버킷 수는 구성된 전략에 따라 결정됩니다(설정 참조).
- 각 key-value 쌍은 키를 해시하여 버킷에 할당됩니다:
bucket = hash(key) % num_buckets. - 각 버킷은 자체 키, 값, 오프셋을 갖는 독립적인 하위 스트림으로 저장됩니다.
buckets_info메타데이터 스트림은 버킷 수와 통계를 기록합니다.
쿼리에서 특정 키(m['key'])를 읽으면 옵티마이저가 해당 식을 키 서브컬럼(m.key_<serialized_key>)으로 재작성합니다.
직렬화 계층은 요청된 키가 어느 버킷에 속하는지 계산하고, 디스크에서 해당 버킷 하나만 읽습니다.
전체 맵을 읽는 경우(예: SELECT m)에는 모든 버킷을 읽어 원래 맵으로 다시 재구성합니다. 이 방식은 여러 하위 스트림을 읽고 머지하는 오버헤드 때문에 basic 직렬화보다 느립니다.
버킷 수는 파트마다 다를 수 있습니다. 버킷 수가 서로 다른 파트를 머지하면 새 파트의 버킷 수는 머지된 통계를 기준으로 다시 계산됩니다. basic 및 with_buckets 직렬화가 적용된 파트는 동일한 테이블에 공존할 수 있으며, 투명하게 머지됩니다.
설정
| 설정 | 기본값 | 설명 |
|---|---|---|
map_serialization_version | basic | 맵(Map) 컬럼의 직렬화 형식입니다. basic은 단일 배열 스트림으로 저장됩니다. with_buckets는 단일 키 읽기 성능을 높이기 위해 키를 버킷으로 분할합니다. |
map_serialization_version_for_zero_level_parts | basic | 0레벨 파트(INSERT로 생성됨)의 직렬화 형식입니다. INSERT 시 쓰기 오버헤드를 피하기 위해 basic을 유지하고, 머지된 파트에는 with_buckets를 사용하도록 할 수 있습니다. |
max_buckets_in_map | 32 | 버킷 수의 상한입니다. 실제 개수는 map_buckets_strategy에 따라 달라집니다. 허용되는 최댓값은 256입니다. |
map_buckets_strategy | sqrt | 평균 맵 크기에서 버킷 수를 계산하는 전략입니다: constant — 항상 max_buckets_in_map을 사용합니다. sqrt — round(coefficient * sqrt(avg_size))를 사용합니다. linear — round(coefficient * avg_size)를 사용합니다. 결과는 [1, max_buckets_in_map] 범위로 제한됩니다. |
map_buckets_coefficient | 1.0 | sqrt 및 linear 전략에 적용되는 계수입니다. 전략이 constant이면 무시됩니다. |
map_buckets_min_avg_size | 32 | 버킷화를 활성화하기 위한 행당 평균 최소 키 수입니다. 평균이 이 임곗값보다 낮으면 다른 설정과 관계없이 단일 버킷이 사용됩니다. 임곗값을 비활성화하려면 0으로 설정하십시오. |
성능 트레이드오프
다음 표는 다양한 맵 크기(행당 키 10개~10,000개)에서 basic 직렬화와 비교한 with_buckets의 성능 영향을 요약합니다. 버킷 수는 최대 32로 제한하는 sqrt 전략으로 결정되었습니다. 정확한 수치는 키/값 타입, 데이터 분포, 하드웨어에 따라 달라집니다.
| 작업 | 키 10개 | 키 100개 | 키 1,000개 | 키 10,000개 | 참고 |
|---|---|---|---|---|---|
단일 키 조회 (m['key']) | 1.6–3.2배 더 빠름 | 4.5–7.7배 더 빠름 | 16–39배 더 빠름 | 21–49배 더 빠름 | 전체 컬럼이 아니라 버킷 1개만 읽습니다. |
| 키 5개 조회 | ~1배 | 1.5–3.1배 더 빠름 | 2.9–8.3배 더 빠름 | 4.5–6.7배 더 빠름 | 각 키는 해당 버킷을 읽으며, 일부 버킷은 서로 겹칠 수 있습니다. |
PREWHERE (SELECT m WHERE m['key'] = ...) | 1.5–3.0배 더 빠름 | 2.9–7.3배 더 빠름 | 5.3–31배 더 빠름 | 20–45배 더 빠름 | PREWHERE 필터는 버킷 1개만 읽고, 전체 맵 읽기는 일치하는 행에 대해서만 수행됩니다. 성능 향상 폭은 선택도에 따라 달라집니다. 즉, 일치하는 그래뉼이 적을수록 전체 맵 I/O가 줄어듭니다. |
전체 맵 스캔 (SELECT m) | ~2배 더 느림 | ~2배 더 느림 | ~2배 더 느림 | ~2배 더 느림 | 모든 버킷을 읽어 다시 조합해야 합니다. |
| INSERT | 1.5–2.5배 더 느림 | 1.5–2.5배 더 느림 | 1.5–2.5배 더 느림 | 1.5–2.5배 더 느림 | 키를 해시하고 여러 하위 스트림에 기록하는 오버헤드가 있습니다. |
권장 사항
- 작은 맵(평균 키 수 32개 미만):
basic직렬화를 유지하십시오. 작은 맵에서는 버킷화에 따른 오버헤드를 감수할 만큼의 이점이 없습니다. 기본값map_buckets_min_avg_size = 32가 이를 자동으로 적용합니다. - 중간 크기 맵(키 32~100개): 쿼리에서 개별 키에 자주 접근한다면
sqrt전략과 함께with_buckets를 사용하십시오. 단일 키 조회는 4~8배 빨라집니다. - 큰 맵(키 100개 이상):
with_buckets를 사용하십시오. 단일 키 조회는 16~49배 빨라집니다. 삽입 속도를 기준값에 가깝게 유지하려면map_serialization_version_for_zero_level_parts = 'basic'설정을 고려하십시오. - 전체 맵 스캔이 주된 워크로드인 경우:
basic을 유지하십시오. 버킷 기반 직렬화는 전체 스캔 시 약 2배의 오버헤드를 추가합니다. - 혼합 워크로드(일부는 키 조회, 일부는 전체 스캔): zero-level 파트를
basic으로 설정한with_buckets를 사용하십시오.PREWHERE최적화는 필터와 관련된 버킷만 먼저 읽고, 그다음 일치하는 행에 대해서만 전체 맵을 읽으므로 전체적으로 상당한 성능 향상을 제공합니다.
대체 접근 방식
버킷 기반 맵 직렬화가 사용 사례에 적합하지 않다면, 키 수준 접근 성능을 개선할 수 있는 대체 접근 방식이 두 가지 있습니다:
JSON 데이터 타입 사용
JSON 데이터 타입은 자주 나타나는 각 경로를 별도의 동적 서브컬럼으로 저장합니다. max_dynamic_paths 제한을 초과하는 경로는 공유 데이터 구조에 저장되며, 여기서는 단일 경로 읽기를 최적화하기 위해 advanced 직렬화를 사용할 수 있습니다. advanced 직렬화에 대한 자세한 내용은 블로그 게시물을 참조하십시오.
| Aspect | 맵 with buckets | JSON |
|---|---|---|
| 단일 키 읽기 | 하나의 버킷을 읽습니다(다른 키가 포함될 수 있음). 버킷 내 모든 key-value 쌍이 역직렬화됩니다. | 자주 나타나는 경로는 동적 서브컬럼에서 직접 읽습니다. 드물게 나타나는 경로는 공유 데이터에 저장되며, advanced 직렬화를 사용하면 해당 경로의 데이터만 읽습니다. |
| 값 타입 | 모든 값이 동일한 타입 V를 공유합니다. | 각 경로는 자체 타입을 가질 수 있습니다. 타입 힌트가 없는 경로는 Dynamic을 사용합니다. |
| 스킵 인덱스 지원 | mapKeys/mapValues에 생성된 일부 인덱스 타입에서 작동합니다. | 스킵 인덱스는 모든 경로/값에 한 번에 생성할 수 없고, 특정 경로 서브컬럼에만 생성할 수 있습니다. |
| 전체 컬럼 읽기 | 버킷을 다시 조립해야 하므로 basic보다 약 2배 느립니다. | Dynamic 타입 인코딩과 경로 재구성에 따른 오버헤드가 있습니다. |
| 스토리지 오버헤드 | 추가 메타데이터가 거의 없습니다. | Dynamic 타입 인코딩, 경로 이름 저장, 그리고 advanced 직렬화의 추가 메타데이터로 인해 더 큽니다. |
| 스키마 유연성 | 테이블 생성 시 키와 값 타입이 고정됩니다. | 완전히 동적이므로 키와 값 타입이 행마다 달라질 수 있습니다. 알려진 경로에 대해서는 타입이 지정된 경로 힌트를 선언할 수 있습니다. |
서로 다른 키에 서로 다른 값 타입이 필요하거나, 키 집합이 행마다 크게 달라지거나, 자주 액세스하는 키를 미리 알고 있어 직접 서브컬럼으로 접근할 수 있도록 타입이 지정된 경로로 선언할 수 있는 경우 JSON을 사용하십시오.
여러 맵 컬럼으로 수동 분할
애플리케이션에서 키 해시를 기준으로 단일 맵을 여러 컬럼으로 직접 분할할 수 있습니다:
삽입 시 각 key-value 쌍을 컬럼 m{hash(key) % 4}로 라우팅합니다. 쿼리 시에는 해당 컬럼에서 읽습니다: m{hash('target_key') % 4}['target_key'].
| 측면 | 버킷이 있는 맵 | 수동 세그먼트 분할 |
|---|---|---|
| 사용 편의성 | 투명하게 처리됨 — 스토리지 엔진이 처리함 | 삽입 및 조회를 위해 애플리케이션 수준의 라우팅 로직이 필요함 |
| 수직 병합 | 지원되지 않음 — 모든 버킷이 하나의 컬럼에 속함 | 지원됨 — 각 맵 컬럼은 독립적인 컬럼이므로 수직 병합 가능함 |
| 스키마 변경 | 버킷 수는 각 파트별로 자동 조정됨 | 세그먼트 수를 변경하려면 데이터를 다시 쓰거나 새 컬럼을 추가해야 함 |
| 쿼리 구문 | m['key']를 바로 사용할 수 있음 | 올바른 컬럼을 계산해야 함: m0['key'], m1['key'] 등 |
| 버킷 세분성 | 파트별이며 데이터 통계에 맞게 조정됨 | 테이블 생성 시 고정됨 |
수동 세그먼트 분할은 컬럼이 많은 테이블을 병합할 때 메모리 사용량을 줄이기 위해 수직 병합이 중요한 경우, 또는 세그먼트 수를 고정하고 명시적으로 제어해야 하는 경우에 유용합니다. 대부분의 사용 사례에서는 자동 버킷 기반 직렬화가 더 단순하며 충분합니다.
함께 보기
- map() 함수
- CAST() 함수
- Map 데이터 타입용 -Map 조합자