Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

在 ClickHouse 中使用 Map 类型

All quickstarts
可观测性OSS

前置条件

你将构建的内容

在 OpenTelemetry 中,每个 trace span 都会携带一组资源属性——也就是描述产生遥测数据的实体的键值元数据 (如服务名称、主机、云区域、Kubernetes pod (容器组) 等) 。这些键的集合会因服务和环境而异,因此很适合使用 ClickHouse 的 Map 类型:键是动态的,并且因应用而异,但任意一行通常只包含其中少数几个。

在本快速入门中,你将使用 clickhouse-local 将来自 CSV file 的真实 OTel trace 数据加载到包含 Map(LowCardinality(String), String) 列的表中,并学习如何对 map 数据进行查询、过滤、聚合和优化。

下载示例数据

该数据集包含从一个演示微服务应用导出的 6,120 个 OTel trace span。每一行都包含 ResourceAttributesSpanAttributes 两列,这两列中是以 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,并创建下列表,其 schema 与 CSV 相匹配。 关键列是 ResourceAttributes Map(LowCardinality(String), String)——在键类型上使用 LowCardinality,因为 OTel attribute 键通常来自一个相对较小且会重复出现的集合。

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 行。

查询数据

访问特定键 — 使用方括号语法从 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 值过滤 — 查找特定服务名称的所有 spans:

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

检查某个键是否存在 —— 并非每个 span 都带有 Kubernetes 元数据。使用 mapContains 找出哪些 span 带有这些元数据:

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 将 map 展开成多行 — 让每个键值对各自成为一行,便于构建属性清单或为仪表盘提供数据:

SELECT
    ServiceName,
    key,
    value
FROM otel_traces
ARRAY JOIN
    mapKeys(ResourceAttributes)  AS key,
    mapValues(ResourceAttributes) AS value
WHERE ServiceName = 'cartservice'
LIMIT 20;

使用 mapFilter 过滤 Map — 仅提取每个 span 中与 Kubernetes 相关的属性:

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

查找错误 span 及其资源上下文 — 将常规列过滤条件与 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 进行聚合

ClickHouse 的 -Map 聚合组合器可让你将任意聚合函数应用到 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 后缀可用于任何聚合函数,因此你同样可以轻松使用 minMapmaxMapavgMap

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。对于任何经常查询的 attribute,这都是 OTel ClickHouse schema 中推荐的模式。

关键要点

  • Map(LowCardinality(String), String) 是 OTel 属性的惯用类型——既足够灵活,能处理不断变化的键集合,又能借助 LowCardinality 保持键存储的高效性。
  • 方括号语法 (map['key']) 是访问值最常见的方式,但要记住它是线性扫描——对于只有几十个键的 Map 完全没问题,但对于几百个键就不太理想了。
  • 物化列是解决之道:当某个 map 键变成高频过滤目标时,可将其提升为真正的列,以获得带索引的列式访问。
  • mapContainsmapKeysmapValuesmapFilterARRAY JOIN 为你提供了一套强大的工具,让你无需离开 SQL 就能探索和转换 Map 数据。
  • -Map 聚合组合器 (sumMapavgMapmaxMap 等) 会跨行按键分别聚合——非常适合对 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