Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

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

All quickstarts
ОбсервабилитиOSS

Предварительные требования

Что вы создадите

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

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

Скачайте пример данных

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

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. Укажите путь к файлу, который вы сохранили:

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

Убедитесь, что данные загружены:

SELECT count() FROM otel_traces;

Вы должны увидеть 6 120 строк.

Запросите данные

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

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

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

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

Разверните Map в строки с помощью ARRAY JOIN — превратите каждую пару ключ-значение в отдельную строку — это удобно для составления перечней атрибутов или наполнения панелей мониторинга:

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:

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

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

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 с помощью комбинатора -Map

Агрегатный комбинатор -Map в ClickHouse позволяет применять любую агрегатную функцию к столбцу типа Map, при этом она выполняется отдельно для каждого ключа. Результатом тоже будет Map — по одной записи на ключ с агрегированным значением. Это особенно полезно для метрик OTel, где значения Counter или Gauge хранятся в Map.

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

Вы также можете комбинировать его с другими комбинаторами. Например, 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, без разворота в строки и без необходимости заранее знать полный набор ключей. Любой ключ, встречающийся хотя бы в одной строке, автоматически включается в результат.

Оптимизируйте работу с часто используемыми в запросах ключами

Если вам постоянно приходится фильтровать данные по одному и тому же ключу в Map — часто это host.name — его можно вынести в материализованный столбец. Это позволит избежать линейного прохода по Map при каждом запросе:

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

Для существующих данных дозагрузите столбец:

ALTER TABLE otel_traces MATERIALIZE COLUMN HostName;

Теперь WHERE HostName = 'prod-cart-01' считывает один выделенный столбец вместо всего map. Это рекомендуемый подход в схеме OTel ClickHouse для любых атрибутов, по которым вы часто выполняете запросы.

Ключевые выводы

  • Map(LowCardinality(String), String) — идиоматический тип для атрибутов OTel: он достаточно гибок, чтобы работать с меняющимися наборами ключей, а LowCardinality позволяет хранить ключи эффективно.
  • Синтаксис с квадратными скобками (map['key']) — самый распространённый способ доступа к значениям, но помните, что он выполняет линейный проход: для map с десятками ключей это нормально, а вот для сотен — уже не лучший вариант.
  • Материализованные столбцы — это выход из ситуации: когда ключ map становится частой целью фильтрации, вынесите его в отдельный столбец для индексированного, столбцового доступа.
  • mapContains, mapKeys, mapValues, mapFilter и ARRAY JOIN дают богатый набор инструментов для изучения и преобразования данных map, не выходя из 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