Предварительные требования
- На вашем компьютере должен быть установлен clickhouse-local. Чтобы начать работу, см. руководство по настройке clickhouse-local.
Что вы создадите
В 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).
Что дальше
Далее ознакомьтесь со следующими руководствами по быстрому старту:
- Создайте свою первую таблицу семейства MergeTree
- Создайте свое первое materialized view
- Типичные проблемы при начале работы
Или перейдите к более подробной справочной документации:
