> ## 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.

# Работа с типом Map в ClickHouse

> Узнайте, как использовать тип Map в ClickHouse для хранения, запросов и агрегации динамических данных в формате ключ-значение на практическом примере атрибутов ресурса в OTel.

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/ru/concepts/features/tools-and-utilities/clickhouse-local).

<div id="what-youll-build">
  ## Что вы создадите
</div>

В OpenTelemetry каждый спан трассировки несёт набор **атрибутов ресурса** — метаданных в формате ключ-значение, описывающих сущность, которая создала телеметрию (имя сервиса, хост, регион облака, под Kubernetes и т. д.). Набор ключей различается в зависимости от сервиса и окружения, поэтому для таких данных естественно подходит тип `Map` в ClickHouse: ключи динамические и зависят от приложения, но в каждой строке их обычно лишь несколько.

В этом кратком руководстве вы будете использовать `clickhouse-local`, чтобы загрузить реальные данные OTel-трассировки из CSV-файла в таблицу со столбцами `Map(LowCardinality(String), String)`, а также научитесь выполнять запросы, фильтровать, агрегировать и оптимизировать данные в `Map`.

<Steps titleSize="h3">
  <Step title="Скачайте пример данных" id="download-the-sample-data">
    Набор данных содержит 6 120 спанов трассировки OTel, экспортированных из демонстрационного микросервисного приложения. Каждая строка включает столбцы `ResourceAttributes` и `SpanAttributes` с динамическими парами ключ-значение в формате JSON.
    Сохраните файл в каталог, путь к которому вам будет удобно использовать, например `~/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)`; `LowCardinality` используется для типа ключа, поскольку ключи атрибутов OTel берутся из относительно небольшого набора повторяющихся значений.

    ```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));
    ```

    Теперь загрузите CSV с помощью движка таблицы `file`. Укажите путь к файлу, который вы сохранили:

    ```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">
    **Доступ к конкретному ключу** — используйте синтаксис с квадратными скобками, чтобы получить значение из `Map`. Если в данной строке такого ключа нет, будет возвращено значение по умолчанию для типа значения (пустая строка для `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;
    ```

    **Фильтрация по значению в map** — найдите все спаны с определённым именем сервиса:

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

    **Проверьте, есть ли ключ** — не каждый span содержит метаданные 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;
    ```

    **Разверните Map в строки с помощью 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;
    ```

    **Фильтруйте maps с помощью 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;
    ```

    **Находите спаны с ошибками и их ресурсный контекст** — сочетайте обычные фильтры по столбцам с доступом к Map:

    ```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 с помощью комбинатора -Map" id="aggregate-across-maps-with-the--map-combinator">
    Агрегатный комбинатор `-Map` в ClickHouse позволяет применять любую агрегатную функцию к столбцу типа `Map`, при этом она выполняется отдельно для каждого ключа. Результатом тоже будет `Map` — по одной записи на ключ с агрегированным значением. Это особенно полезно для метрик OTel, где значения Counter или Gauge хранятся в `Map`.

    Чтобы продемонстрировать это, создайте небольшую таблицу метрик, в которой каждая строка содержит количества 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;
    ```

    Вы также можете комбинировать его с другими комбинаторами. Например, `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">
    Если вам постоянно приходится фильтровать данные по одному и тому же ключу в Map — часто это `host.name` — его можно вынести в материализованный столбец. Это позволит избежать линейного прохода по Map при каждом запросе:

    ```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'` считывает один выделенный столбец вместо всего map. Это рекомендуемый подход в схеме OTel ClickHouse для любых атрибутов, по которым вы часто выполняете запросы.
  </Step>
</Steps>

<div id="key-takeaways">
  ## Ключевые выводы
</div>

* **`Map(LowCardinality(String), String)`** — идиоматический тип для атрибутов OTel: он достаточно гибок, чтобы работать с меняющимися наборами ключей, а `LowCardinality` позволяет хранить ключи эффективно.
* **Синтаксис с квадратными скобками** (`map['key']`) — самый распространённый способ доступа к значениям, но помните, что он выполняет линейный проход: для map с десятками ключей это нормально, а вот для сотен — уже не лучший вариант.
* **Материализованные столбцы** — это выход из ситуации: когда ключ map становится частой целью фильтрации, вынесите его в отдельный столбец для индексированного, столбцового доступа.
* **`mapContains`, `mapKeys`, `mapValues`, `mapFilter`** и `ARRAY JOIN` дают богатый набор инструментов для изучения и преобразования данных map, не выходя из SQL.
* **Агрегатный комбинатор `-Map`** (`sumMap`, `avgMap`, `maxMap` и т. д.) агрегирует каждый ключ независимо по всем строкам — это идеально для свёртки счётчиков метрик OTel, когда набор ключей заранее неизвестен. Он также сочетается с другими комбинаторами (например, `sumMapIf`).

<div id="next-steps">
  ## Что дальше
</div>

Далее ознакомьтесь со следующими руководствами по быстрому старту:

* [Создайте свою первую таблицу семейства MergeTree](/docs/ru/get-started/quickstarts/create-your-first-mergetree-table)
* [Создайте свое первое materialized view](/docs/ru/get-started/quickstarts/create-your-first-materialized-view)
* [Типичные проблемы при начале работы](/docs/ru/get-started/quickstarts/home)

Или перейдите к более подробной справочной документации:

* [Справочник по типу Map](/docs/ru/reference/data-types/map)
* [Экспортер ClickHouse OTel](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/clickhouseexporter)
* [Комбинаторы агрегатных функций](/docs/ru/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>
