Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

ClickHouse에서 맵 타입 사용하기

All quickstarts
관측성OSS

사전 요구 사항

구축할 내용

OpenTelemetry에서는 모든 트레이스 스팬이 리소스 속성 집합을 가집니다 — 이는 텔레메트리를 생성한 엔터티(service name, host, cloud region, Kubernetes 파드 등)를 설명하는 키-값 메타데이터입니다. 키 집합은 서비스와 환경에 따라 달라지므로 ClickHouse의 Map 타입에 잘 맞습니다. 키는 동적이고 애플리케이션별로 달라지지만, 각 행에는 보통 몇 개만 들어 있습니다.

이 빠른 시작에서는 clickhouse-local을 사용해 CSV 파일의 실제 OTel 트레이스 데이터를 Map(LowCardinality(String), String) 컬럼이 있는 테이블에 적재하고, 맵 데이터를 쿼리, 필터링, 집계, 최적화하는 방법을 알아봅니다.

샘플 데이터 다운로드

이 데이터셋에는 데모 마이크로서비스 애플리케이션에서 내보낸 6,120개의 OTel trace 스팬이 포함되어 있습니다. 각 행에는 JSON 맵 형태의 동적 key-value 쌍이 포함된 ResourceAttributesSpanAttributes 컬럼이 있습니다. 파일은 예를 들어 ~/data/data-otel-traces.csv처럼 쉽게 참조할 수 있는 디렉터리에 저장하십시오.

data-otel-traces.csv 다운로드 (2.9 MB)

단일 행은 다음과 같습니다:

Timestamp:          2025-12-26 00:00:45.759467000
TraceId:            0da128e6e3c01bc38b6b43a33e5fa522
SpanId:             3774f759424e4006
ParentSpanId:       2fdd1e5b66605098
SpanName:           orders receive
SpanKind:           SPAN_KIND_CONSUMER
ServiceName:        accountingservice
Duration:           5361
StatusCode:         STATUS_CODE_UNSET
ResourceAttributes: {"host.name":"f19476836e47","os.type":"linux","process.pid":"1","process.command_args":"[\"./accountingservice\"]","process.executable.path":"...
SpanAttributes:     {"network.transport":"tcp","messaging.destination.name":"orders","messaging.kafka.message.offset":"232260","messaging.message.body.size":"216"...

테이블을 생성하고 데이터를 로드합니다

clickhouse-local을 실행한 다음, CSV 스키마에 맞춰 다음 테이블을 생성합니다. 키 컬럼은 ResourceAttributes Map(LowCardinality(String), String)이며, OTel 속성 키는 비교적 적은 수의 반복되는 집합에서 나오므로 키 타입에 LowCardinality를 사용합니다.

CREATE TABLE otel_traces
(
    Timestamp          DateTime64(9),
    TraceId            String,
    SpanId             String,
    ParentSpanId       String,
    SpanName           LowCardinality(String),
    SpanKind           LowCardinality(String),
    ServiceName        LowCardinality(String),
    Duration           UInt64,
    StatusCode         LowCardinality(String),
    ResourceAttributes Map(LowCardinality(String), String),
    SpanAttributes     Map(LowCardinality(String), String)
)
ENGINE = MergeTree()
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp));

이제 file 테이블 엔진을 사용해 CSV를 불러오십시오. 파일을 저장한 위치에 맞게 경로를 조정하십시오:

INSERT INTO otel_traces
SELECT * FROM file('~/data/data-otel-traces.csv', CSVWithNames);

데이터가 로드되었는지 확인하세요:

SELECT count() FROM otel_traces;

6,120개 행이 표시되어야 합니다.

데이터 쿼리

특정 키에 액세스하기 — 대괄호 구문을 사용해 맵에서 값을 꺼낼 수 있습니다. 해당 행에 키가 없으면 값 유형의 기본값이 반환됩니다(String은 빈 문자열):

SELECT
    ServiceName,
    SpanName,
    ResourceAttributes['host.name']             AS host,
    ResourceAttributes['k8s.pod.name']          AS pod,
    ResourceAttributes['deployment.environment'] AS env
FROM otel_traces
LIMIT 10;

맵 값으로 필터링 — 특정 서비스 이름의 모든 스팬 찾기:

SELECT
    Timestamp,
    SpanName,
    Duration / 1e6 AS duration_ms
FROM otel_traces
WHERE ResourceAttributes['service.name'] = 'cartservice'
ORDER BY Timestamp
LIMIT 10;

키 존재 여부 확인 — 모든 스팬에 Kubernetes 메타데이터가 있는 것은 아닙니다. 어떤 스팬에 메타데이터가 있는지 찾으려면 mapContains를 사용합니다:

SELECT
    ServiceName,
    SpanName,
    mapContains(ResourceAttributes, 'k8s.node.name') AS has_node_info
FROM otel_traces
LIMIT 10;

데이터셋 전체에 있는 모든 키 확인 — 어떤 계측 데이터가 생성되는지 파악하는 데 유용합니다:

SELECT DISTINCT arrayJoin(mapKeys(ResourceAttributes)) AS key
FROM otel_traces
ORDER BY key;

ARRAY JOIN으로 맵을 행으로 펼치기 — 각 키-값 쌍을 개별 행으로 변환하므로, 속성 목록을 만들거나 대시보드용 데이터를 구성할 때 유용합니다:

SELECT
    ServiceName,
    key,
    value
FROM otel_traces
ARRAY JOIN
    mapKeys(ResourceAttributes)  AS key,
    mapValues(ResourceAttributes) AS value
WHERE ServiceName = 'cartservice'
LIMIT 20;

mapFilter로 맵 필터링하기 — 각 스팬에서 Kubernetes 관련 속성만 추출합니다:

SELECT
    ServiceName,
    mapFilter((k, v) -> k LIKE 'k8s.%', ResourceAttributes) AS k8s_attrs
FROM otel_traces
WHERE mapContains(ResourceAttributes, 'k8s.pod.name')
LIMIT 10;

오류 스팬과 해당 resource 컨텍스트 찾기 — 일반 컬럼 필터와 맵 접근을 함께 사용합니다:

SELECT
    Timestamp,
    ServiceName,
    SpanName,
    ResourceAttributes['host.name']    AS host,
    ResourceAttributes['k8s.pod.name'] AS pod,
    SpanAttributes['error.type']       AS error_type,
    SpanAttributes['error.message']    AS error_message
FROM otel_traces
WHERE StatusCode = 'STATUS_CODE_ERROR';

-Map 조합자로 맵 전체에 걸쳐 집계하기

ClickHouse의 -Map 집계 조합자를 사용하면 Map 컬럼에 어떤 집계 함수든 적용해 각 키별로 독립적으로 집계할 수 있습니다. 결과 역시 Map이며, 각 키마다 하나의 항목과 해당 집계값이 포함됩니다. 이는 카운터나 게이지가 맵 값으로 저장되는 OTel 메트릭에서 특히 유용합니다.

이를 보여주기 위해, 각 행이 HTTP 상태 코드별 개수를 Map(String, UInt64)로 기록하는 작은 메트릭 테이블을 생성합니다:

CREATE TABLE otel_http_status_counts
(
    Timestamp    DateTime,
    ServiceName  LowCardinality(String),
    StatusCounts Map(String, UInt64)
)
ENGINE = MergeTree()
ORDER BY (ServiceName, Timestamp);

INSERT INTO otel_http_status_counts VALUES
    ('2025-12-26 10:00:00', 'cart-service',      {'2xx': 150, '4xx': 12, '5xx': 3}),
    ('2025-12-26 10:01:00', 'cart-service',      {'2xx': 200, '4xx': 8,  '5xx': 1}),
    ('2025-12-26 10:00:00', 'inventory-service', {'2xx': 90,  '4xx': 5}),
    ('2025-12-26 10:01:00', 'inventory-service', {'2xx': 110, '4xx': 3,  '5xx': 2}),
    ('2025-12-26 10:00:00', 'payment-service',   {'2xx': 50,  '5xx': 10}),
    ('2025-12-26 10:01:00', 'payment-service',   {'2xx': 45,  '4xx': 2,  '5xx': 15});

이제 sumMap을 사용해 각 서비스의 상태 코드별 개수를 합산합니다:

SELECT
    ServiceName,
    sumMap(StatusCounts) AS total_by_status
FROM otel_http_status_counts
GROUP BY ServiceName;

-Map 접미사는 모든 집계 함수와 함께 사용할 수 있으므로 minMap, maxMap, avgMap도 같은 방식으로 쉽게 사용할 수 있습니다:

SELECT
    ServiceName,
    avgMap(StatusCounts) AS avg_by_status,
    maxMap(StatusCounts) AS peak_by_status
FROM otel_http_status_counts
GROUP BY ServiceName;

다른 combinator와 함께 사용할 수도 있습니다. 예를 들어 sumMapIf를 사용하면 조건에 따라 집계할 수 있습니다. 여기서는 서비스에 이미 오류가 있었던 분 단위 윈도우만 합산합니다:

SELECT
    ServiceName,
    sumMapIf(StatusCounts, StatusCounts['5xx'] > 0) AS totals_in_error_windows
FROM otel_http_status_counts
GROUP BY ServiceName;

OTel에서 이것이 중요한 이유: OTel Collector가 분 단위 상태 코드별 집계를 ClickHouse에 기록할 때 sumMap을 사용하면 이를 단일 쿼리로 시간별 또는 일별 합계로 집계할 수 있습니다 — ARRAY JOIN도, 언피벗도, 전체 키 집합을 미리 파악하는 작업도 필요하지 않습니다. 어떤 키든 하나의 행에라도 나타나면 결과에 자동으로 포함됩니다.

자주 쿨리하는 키에 맞춰 최적화

같은 맵 키로 계속 필터링하는 경우 — host.name가 대표적인 예입니다 — 이를 구체화된 컬럼(Materialized Column)으로 추출할 수 있습니다. 이렇게 하면 쿼리할 때마다 맵 전체를 선형 스캔하지 않아도 됩니다:

ALTER TABLE otel_traces
    ADD COLUMN HostName String
    MATERIALIZED ResourceAttributes['host.name'];

기존 데이터에도 컬럼을 백필하세요:

ALTER TABLE otel_traces MATERIALIZE COLUMN HostName;

이제 WHERE HostName = 'prod-cart-01'는 전체 맵 대신 해당 전용 단일 컬럼만 읽습니다. 이는 자주 쿼리하는 속성에 대해 OTel ClickHouse 스키마(schema)에서 권장되는 패턴입니다.

핵심 요약

  • **Map(LowCardinality(String), String)**은 OTel 속성에 가장 자연스럽게 쓰이는 타입입니다. 다양한 키 집합을 유연하게 처리할 수 있고, LowCardinality는 키 저장 효율도 높여 줍니다.
  • 대괄호 구문 (map['key'])은 값을 조회하는 가장 일반적인 방법이지만, 선형 스캔을 수행한다는 점을 기억해야 합니다. 키가 수십 개인 맵에는 괜찮지만, 수백 개라면 적합하지 않을 수 있습니다.
  • materialized 컬럼은 이런 상황에서 유용한 해결책입니다. 맵 키가 자주 사용하는 필터 대상이 되면 이를 실제 컬럼으로 승격해 인덱스 기반의 열 지향 액세스를 활용하십시오.
  • mapContains, mapKeys, mapValues, mapFilterARRAY JOIN은 SQL을 벗어나지 않고도 맵 데이터를 탐색하고 변환할 수 있는 강력한 도구를 제공합니다.
  • -Map 집계 조합자 (sumMap, avgMap, maxMap 등)는 행 전반에서 각 키를 독립적으로 집계합니다. 키 집합을 미리 알 필요 없이 OTel 메트릭 카운터를 롤업하는 데 적합합니다. 다른 조합자와도 함께 조합할 수 있습니다(예: sumMapIf).

다음 단계

다음 빠른 시작도 확인해 보세요:

또는 참고 문서를 더 자세히 살펴보세요:

ClickHouse Academy — Master ClickHouse with expert-designed training for every skill level
Check out the ClickHouse academy for on-demand and live training
Navigation