> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# ClickHouse에서 맵 타입 사용하기

> OTel 리소스 속성을 예시로, ClickHouse에서 동적 키-값 데이터를 저장, 쿼리, 집계하는 데 맵 타입을 사용하는 방법을 알아봅니다.

export const e_1 = undefined

export const e_0 = undefined

<a href="/docs/get-started/quickstarts/home" onClick={(e_0) => { e_0.preventDefault(); window.location.href = (window.location.pathname.startsWith('/docs') ? '/docs' : '') + '/get-started/quickstarts/home'; }} className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-zinc-500 hover:text-gray-900 dark:hover:text-[#fdff75] transition-colors font-normal no-underline"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0"><path d="M19 12H5" /><path d="M12 19l-7-7 7-7" /></svg>All quickstarts</a>

<div className="mt-2 flex flex-wrap gap-2">
  <Badge size="lg" color="blue">관측성</Badge>
  <Badge size="lg" color="orange">OSS</Badge>
</div>

<div id="prerequisites">
  ## 사전 요구 사항
</div>

* **clickhouse-local**이 로컬 머신에 설치되어 있어야 합니다. 시작하려면 [clickhouse-local 설정 가이드](/docs/ko/concepts/features/tools-and-utilities/clickhouse-local)를 참조하십시오.

<div id="what-youll-build">
  ## 구축할 내용
</div>

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

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

<Steps titleSize="h3">
  <Step title="샘플 데이터 다운로드" id="download-the-sample-data">
    이 데이터셋에는 데모 마이크로서비스 애플리케이션에서 내보낸 6,120개의 OTel trace 스팬이 포함되어 있습니다. 각 행에는 JSON 맵 형태의 동적 key-value 쌍이 포함된 `ResourceAttributes` 및 `SpanAttributes` 컬럼이 있습니다.
    파일은 예를 들어 `~/data/data-otel-traces.csv`처럼 쉽게 참조할 수 있는 디렉터리에 저장하십시오.

    <a href="https://clickhouse-docs-assets.s3.us-east-1.amazonaws.com/data-otel-traces.csv" download className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-lg border border-gray-300 dark:border-white/20 bg-white dark:bg-[#1B1B18] text-black dark:text-white hover:border-[#FAFF69] transition-all no-underline mb-4">
      <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M8 1v10M8 11L4.5 7.5M8 11l3.5-3.5M2 13h12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
      </svg>

      data-otel-traces.csv 다운로드 (2.9 MB)
    </a>

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

    ```response theme={null}
    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"...
    ```
  </Step>

  <Step title="테이블을 생성하고 데이터를 로드합니다" id="create-the-table-and-load-the-data">
    `clickhouse-local`을 실행한 다음, CSV 스키마에 맞춰 다음 테이블을 생성합니다.
    키 컬럼은 `ResourceAttributes Map(LowCardinality(String), String)`이며, OTel 속성 키는 비교적 적은 수의 반복되는 집합에서 나오므로 키 타입에 `LowCardinality`를 사용합니다.

    ```sql highlight={12} theme={null}
    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를 불러오십시오. 파일을 저장한 위치에 맞게 경로를 조정하십시오:

    ```sql theme={null}
    INSERT INTO otel_traces
    SELECT * FROM file('~/data/data-otel-traces.csv', CSVWithNames);
    ```

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

    ```sql theme={null}
    SELECT count() FROM otel_traces;
    ```

    6,120개 행이 표시되어야 합니다.
  </Step>

  <Step title="데이터 쿼리" id="query-the-data">
    **특정 키에 액세스하기** — 대괄호 구문을 사용해 맵에서 값을 꺼낼 수 있습니다. 해당 행에 키가 없으면 값 유형의 기본값이 반환됩니다(`String`은 빈 문자열):

    ```sql theme={null}
    SELECT
        ServiceName,
        SpanName,
        ResourceAttributes['host.name']             AS host,
        ResourceAttributes['k8s.pod.name']          AS pod,
        ResourceAttributes['deployment.environment'] AS env
    FROM otel_traces
    LIMIT 10;
    ```

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

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

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

    ```sql theme={null}
    SELECT
        ServiceName,
        SpanName,
        mapContains(ResourceAttributes, 'k8s.node.name') AS has_node_info
    FROM otel_traces
    LIMIT 10;
    ```

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

    ```sql theme={null}
    SELECT DISTINCT arrayJoin(mapKeys(ResourceAttributes)) AS key
    FROM otel_traces
    ORDER BY key;
    ```

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

    ```sql theme={null}
    SELECT
        ServiceName,
        key,
        value
    FROM otel_traces
    ARRAY JOIN
        mapKeys(ResourceAttributes)  AS key,
        mapValues(ResourceAttributes) AS value
    WHERE ServiceName = 'cartservice'
    LIMIT 20;
    ```

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

    ```sql theme={null}
    SELECT
        ServiceName,
        mapFilter((k, v) -> k LIKE 'k8s.%', ResourceAttributes) AS k8s_attrs
    FROM otel_traces
    WHERE mapContains(ResourceAttributes, 'k8s.pod.name')
    LIMIT 10;
    ```

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

    ```sql theme={null}
    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';
    ```
  </Step>

  <Step title="-Map 조합자로 맵 전체에 걸쳐 집계하기" id="aggregate-across-maps-with-the--map-combinator">
    ClickHouse의 `-Map` 집계 조합자를 사용하면 `Map` 컬럼에 어떤 집계 함수든 적용해 각 키별로 독립적으로 집계할 수 있습니다. 결과 역시 `Map`이며, 각 키마다 하나의 항목과 해당 집계값이 포함됩니다. 이는 카운터나 게이지가 맵 값으로 저장되는 OTel 메트릭에서 특히 유용합니다.

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

    ```sql theme={null}
    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`을 사용해 각 서비스의 상태 코드별 개수를 합산합니다:

    ```sql theme={null}
    SELECT
        ServiceName,
        sumMap(StatusCounts) AS total_by_status
    FROM otel_http_status_counts
    GROUP BY ServiceName;
    ```

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

    ```sql theme={null}
    SELECT
        ServiceName,
        avgMap(StatusCounts) AS avg_by_status,
        maxMap(StatusCounts) AS peak_by_status
    FROM otel_http_status_counts
    GROUP BY ServiceName;
    ```

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

    ```sql theme={null}
    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`도, 언피벗도, 전체 키 집합을 미리 파악하는 작업도 필요하지 않습니다. 어떤 키든 하나의 행에라도 나타나면 결과에 자동으로 포함됩니다.
  </Step>

  <Step title="자주 쿨리하는 키에 맞춰 최적화" id="optimise-for-frequently-queried-keys">
    같은 맵 키로 계속 필터링하는 경우 — `host.name`가 대표적인 예입니다 — 이를 구체화된 컬럼(Materialized Column)으로 추출할 수 있습니다. 이렇게 하면 쿼리할 때마다 맵 전체를 선형 스캔하지 않아도 됩니다:

    ```sql theme={null}
    ALTER TABLE otel_traces
        ADD COLUMN HostName String
        MATERIALIZED ResourceAttributes['host.name'];
    ```

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

    ```sql theme={null}
    ALTER TABLE otel_traces MATERIALIZE COLUMN HostName;
    ```

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

<div id="key-takeaways">
  ## 핵심 요약
</div>

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

<div id="next-steps">
  ## 다음 단계
</div>

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

* [첫 번째 MergeTree 테이블 만들기](/docs/ko/get-started/quickstarts/create-your-first-mergetree-table)
* [첫 번째 materialized view 만들기](/docs/ko/get-started/quickstarts/create-your-first-materialized-view)
* [시작하기에서 자주 발생하는 문제](/docs/ko/get-started/quickstarts/home)

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

* [맵 타입 참고](/docs/ko/reference/data-types/map)
* [ClickHouse OTel exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/clickhouseexporter)
* [집계 함수 조합자](/docs/ko/reference/functions/aggregate-functions/combinators)

<Frame caption="Check out the ClickHouse academy for on-demand and live training">
  <a href="https://learn.clickhouse.com/" target="_blank">
    <img src="https://mintcdn.com/private-7c7dfe99/EDr8ydtGBgFPOQea/images/academy.webp?fit=max&auto=format&n=EDr8ydtGBgFPOQea&q=85&s=27e92fc656183cc2f176211907a7aa49" alt="ClickHouse Academy — Master ClickHouse with expert-designed training for every skill level" width="560" noZoom data-path="images/academy.webp" />
  </a>
</Frame>

<div className="mt-8">
  <a href="/docs/get-started/quickstarts/home" onClick={(e_1) => { e_1.preventDefault(); window.location.href = (window.location.pathname.startsWith('/docs') ? '/docs' : '') + '/get-started/quickstarts/home'; }} className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-zinc-500 hover:text-gray-900 dark:hover:text-[#fdff75] transition-colors font-normal no-underline"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0"><path d="M19 12H5" /><path d="M12 19l-7-7 7-7" /></svg>All quickstarts</a>
</div>
