사전 요구 사항
- clickhouse-local이 로컬 머신에 설치되어 있어야 합니다. 시작하려면 clickhouse-local 설정 가이드를 참조하십시오.
구축할 내용
OpenTelemetry에서는 모든 트레이스 스팬이 리소스 속성 집합을 가집니다 — 이는 텔레메트리를 생성한 엔터티(service name, host, cloud region, Kubernetes 파드 등)를 설명하는 키-값 메타데이터입니다. 키 집합은 서비스와 환경에 따라 달라지므로 ClickHouse의 Map 타입에 잘 맞습니다. 키는 동적이고 애플리케이션별로 달라지지만, 각 행에는 보통 몇 개만 들어 있습니다.
이 빠른 시작에서는 clickhouse-local을 사용해 CSV 파일의 실제 OTel 트레이스 데이터를 Map(LowCardinality(String), String) 컬럼이 있는 테이블에 적재하고, 맵 데이터를 쿼리, 필터링, 집계, 최적화하는 방법을 알아봅니다.
샘플 데이터 다운로드
이 데이터셋에는 데모 마이크로서비스 애플리케이션에서 내보낸 6,120개의 OTel trace 스팬이 포함되어 있습니다. 각 행에는 JSON 맵 형태의 동적 key-value 쌍이 포함된 ResourceAttributes 및 SpanAttributes 컬럼이 있습니다.
파일은 예를 들어 ~/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,mapFilter및ARRAY JOIN은 SQL을 벗어나지 않고도 맵 데이터를 탐색하고 변환할 수 있는 강력한 도구를 제공합니다.-Map집계 조합자 (sumMap,avgMap,maxMap등)는 행 전반에서 각 키를 독립적으로 집계합니다. 키 집합을 미리 알 필요 없이 OTel 메트릭 카운터를 롤업하는 데 적합합니다. 다른 조합자와도 함께 조합할 수 있습니다(예:sumMapIf).
다음 단계
다음 빠른 시작도 확인해 보세요:
또는 참고 문서를 더 자세히 살펴보세요:
