Map(K, V)는 키-값 쌍을 저장합니다.
다른 데이터베이스와 달리 ClickHouse에서는 맵의 키가 고유하지 않습니다. 즉, 하나의 맵에 동일한 키를 가진 요소가 두 개 있을 수 있습니다.
(이는 맵이 내부적으로 Array(Tuple(K, V))로 구현되기 때문입니다.)
맵 m에서 키 k의 값을 가져오려면 m[k] 구문을 사용할 수 있습니다.
또한 m[k]는 맵 전체를 스캔하므로, 이 연산의 런타임은 맵 크기에 선형적으로 비례합니다.
매개변수
K— Map 키의 타입입니다. Nullable 및 Nullable 타입이 중첩된 LowCardinality를 제외한 임의의 타입을 사용할 수 있습니다.V— Map 값의 타입입니다. 임의의 타입을 사용할 수 있습니다.
Query
key2의 값을 선택하려면:
Query
Response
k가 맵에 없으면 m[k]는 값 타입의 기본값을 반환합니다. 예를 들어 정수 타입은 0, 문자열 타입은 ''를 반환합니다.
맵에 키가 존재하는지 확인하려면 mapContains 함수를 사용할 수 있습니다.
Query
Response
Tuple을 Map으로 변환하기
Tuple() 타입의 값은 CAST 함수를 사용해 Map() 타입의 값으로 변환할 수 있습니다:
예시
Query
Response
맵의 서브컬럼 읽기
keys와 values를 사용할 수 있습니다.
예시
Query
Response
MergeTree의 버킷형 맵 직렬화
Map 컬럼은 단일 Array(Tuple(K, V)) 스트림으로 저장됩니다.
m['key']로 특정 키 하나를 읽으려면, 하나의 키만 필요하더라도 모든 행의 모든 키-값 쌍이 들어 있는 전체 컬럼을 스캔해야 합니다.
서로 다른 키가 많은 맵에서는 이것이 병목이 됩니다.
버킷형 직렬화(with_buckets)는 키를 해시하여 키-값 쌍을 여러 개의 독립적인 하위 스트림(버킷)으로 분할합니다.
쿼리에서 m['key']에 접근하면 해당 키가 들어 있는 버킷만 디스크에서 읽고, 나머지 버킷은 모두 건너뜁니다.
버킷 단위 직렬화 활성화
INSERT 시 생성됨)에는 basic 직렬화를 유지하고 병합된 파트에만 with_buckets를 사용할 수 있습니다:
동작 방식
with_buckets 직렬화로 기록되면 다음과 같이 동작합니다.
- 행당 평균 키 수를 블록 통계에서 계산합니다.
- 버킷 수는 구성된 전략에 따라 결정됩니다(설정 참조).
- 각 키-값 쌍은 키를 해시하여 버킷에 할당됩니다:
bucket = hash(key) % num_buckets. - 각 버킷은 자체 키, 값, 오프셋을 가진 독립적인 서브스트림으로 저장됩니다.
buckets_info메타데이터 스트림에는 버킷 수와 통계가 기록됩니다.
m['key'])를 읽으면 옵티마이저가 표현식을 키 서브컬럼(m.key_<serialized_key>)으로 재작성합니다.
직렬화 계층은 요청된 키가 어느 버킷에 속하는지 계산하고, 해당 버킷 하나만 디스크에서 읽습니다.
전체 맵을 읽을 때(예: SELECT m)는 모든 버킷을 읽어 원래 맵으로 재구성합니다. 여러 서브스트림을 읽고 머지하는 오버헤드가 있으므로 basic 직렬화보다 더 느립니다.
with_buckets 직렬화를 사용하면 맵 값 내부의 키 순서가 원래 삽입 순서와 달라질 수 있습니다. 키는 해시에 따라 여러 버킷에 분산되며, 삽입 순서가 아니라 버킷 순서대로 다시 조합됩니다. basic 직렬화에서는 삽입된 맵의 키 순서가 유지됩니다.basic 직렬화와 with_buckets 직렬화는 같은 테이블에 함께 존재할 수 있으며, 머지 시에도 투명하게 처리됩니다.
설정
성능 트레이드오프
with_buckets를 basic 직렬화(serialization)와 비교했을 때의 성능 영향을 요약합니다. 버킷 수는 최대 32로 제한하는 sqrt 전략으로 결정되었습니다. 정확한 수치는 키/값 타입, 데이터 분포, 하드웨어에 따라 달라집니다.
권장 사항
- 작은 맵(평균 키 수 32개 미만):
basic직렬화를 유지하십시오. 작은 맵에는 버킷 사용에 따른 오버헤드가 크지 않아 이 방식이 더 적합합니다. 기본값map_buckets_min_avg_size = 32가 이를 자동으로 적용합니다. - 중간 크기 맵(키 32~100개): 쿼리에서 개별 키에 자주 접근한다면
sqrt전략과 함께with_buckets를 사용하십시오. 단일 키 lookup은 4~8배 빨라집니다. - 큰 맵(키 100개 이상):
with_buckets를 사용하십시오. 단일 키 lookup은 16~49배 더 빠릅니다. 삽입 속도를 기준 수준에 가깝게 유지하려면map_serialization_version_for_zero_level_parts = 'basic'사용을 고려하십시오. - 전체 맵 스캔이 워크로드의 대부분을 차지하는 경우:
basic을 유지하십시오. 버킷 직렬화는 전체 스캔에서 약 2배의 오버헤드를 추가합니다. - 혼합 워크로드(일부 키 lookup, 일부 전체 스캔): 제로 수준 파트를
basic으로 설정한with_buckets를 사용하십시오.PREWHERE최적화는 필터에 해당하는 버킷만 먼저 읽고, 그다음 일치하는 행에 대해서만 전체 맵을 읽으므로 전체적으로 상당한 성능 향상을 얻을 수 있습니다.
대체 접근 방식
Map 직렬화가 사용 사례에 맞지 않는다면, 키 수준 접근 성능을 개선할 수 있는 대체 방법은 두 가지가 있습니다:
JSON 데이터 타입 사용
max_dynamic_paths 제한을 초과하는 경로는 공유 데이터 구조로 들어가며, 여기서는 단일 경로 읽기를 최적화하기 위해 advanced 직렬화를 사용할 수 있습니다. advanced 직렬화에 대한 자세한 내용은 블로그 글을 참조하십시오.
서로 다른 키에 서로 다른 값 타입이 필요하거나, 행마다 키 집합이 크게 달라지거나, 자주 접근하는 키를 미리 알고 있어 직접 서브컬럼에 접근할 수 있도록 타입이 지정된 경로로 선언할 수 있는 경우에는
JSON을 사용하십시오.
여러 맵 컬럼으로 수동 세그먼트 분할
Map을 여러 컬럼으로 수동으로 나눌 수 있습니다:
m{hash(key) % 4}로 라우팅합니다. 쿼리 시에는 해당 컬럼에서 읽습니다: m{hash('target_key') % 4}['target_key'].
수동 세그먼트 분할은 컬럼이 많은 테이블을 머지할 때 메모리 사용량을 줄이기 위해 수직 병합이 중요하거나, 세그먼트 수를 고정하고 명시적으로 제어해야 할 때 유용합니다. 대부분의 사용 사례에서는 자동 버킷 직렬화가 더 단순하며 충분합니다.
관련 항목