> ## 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 中使用 Map 类型

> 了解如何在 ClickHouse 中使用 Map 类型来存储、查询和聚合动态键值数据，并以 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/zh/concepts/features/tools-and-utilities/clickhouse-local) 了解如何开始使用。

<div id="what-youll-build">
  ## 你将构建的内容
</div>

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

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

<Steps titleSize="h3">
  <Step title="下载示例数据" id="download-the-sample-data">
    该数据集包含从一个演示微服务应用导出的 6,120 个 OTel trace span。每一行都包含 `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`，并创建下列表，其 schema 与 CSV 相匹配。
    关键列是 `ResourceAttributes Map(LowCardinality(String), String)`——在键类型上使用 `LowCardinality`，因为 OTel attribute 键通常来自一个相对较小且会重复出现的集合。

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

    ```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` 找出哪些 `span` 带有这些元数据：

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

    ```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` 过滤 Map** — 仅提取每个 span 中与 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;
    ```

    **查找错误 span 及其资源上下文** — 将常规列过滤条件与 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">
    ClickHouse 的 `-Map` 聚合组合器可让你将任意聚合函数应用到 `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。对于任何经常查询的 attribute，这都是 OTel ClickHouse schema 中推荐的模式。
  </Step>
</Steps>

<div id="key-takeaways">
  ## 关键要点
</div>

* **`Map(LowCardinality(String), String)`** 是 OTel 属性的惯用类型——既足够灵活，能处理不断变化的键集合，又能借助 `LowCardinality` 保持键存储的高效性。
* **方括号语法** (`map['key']`) 是访问值最常见的方式，但要记住它是线性扫描——对于只有几十个键的 Map 完全没问题，但对于几百个键就不太理想了。
* **物化列**是解决之道：当某个 map 键变成高频过滤目标时，可将其提升为真正的列，以获得带索引的列式访问。
* **`mapContains`、`mapKeys`、`mapValues`、`mapFilter`** 和 `ARRAY JOIN` 为你提供了一套强大的工具，让你无需离开 SQL 就能探索和转换 Map 数据。
* **`-Map` 聚合组合器** (`sumMap`、`avgMap`、`maxMap` 等) 会跨行按键分别聚合——非常适合对 OTel 指标计数器进行汇总，而无需预先知道键集合。它还可以与其他组合器配合使用 (例如 `sumMapIf`) 。

<div id="next-steps">
  ## 后续步骤
</div>

接下来可查看以下快速入门：

* [创建你的第一个 MergeTree 表](/docs/zh/get-started/quickstarts/create-your-first-mergetree-table)
* [创建你的第一个 materialized view](/docs/zh/get-started/quickstarts/create-your-first-materialized-view)
* [常见入门问题](/docs/zh/get-started/quickstarts/home)

或者深入阅读以下参考文档：

* [Map 类型参考](/docs/zh/reference/data-types/map)
* [ClickHouse OTel 导出器](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/clickhouseexporter)
* [聚合函数组合器](/docs/zh/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>
