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

# 运行手册：JSON schema

> 为 ClickHouse 中的 JSON 数据选择合适的 schema 方案——类型化列、混合方案、原生 JSON 或 String 存储

<Note>
  JSON 列类型从 ClickHouse 25.3+ 开始已可用于生产环境。不建议在生产环境中使用更早的版本。
</Note>

如果你的数据以 JSON 形式写入，ClickHouse 提供了多种存储方式，从完全类型化的列到原始 String。具体选择取决于你的 schema 结构有多稳定，以及你是否需要字段级查询。

**范围：** 本页介绍存储 JSON 数据时的 schema 设计决策。不涵盖 [JSON 输入/输出格式](/docs/zh/reference/formats/JSON/JSON)、[JSON 函数](/docs/zh/reference/functions/regular-functions/json-functions) 或查询语法。有关 JSON 列类型本身的更多背景信息，请参见 [在适当情况下使用 JSON](/docs/zh/concepts/best-practices/json-type)。

**前提：** 你应熟悉 [ClickHouse 表创建](/docs/zh/reference/statements/create/table)、[MergeTree](/docs/zh/reference/engines/table-engines/mergetree-family/mergetree) 的基础知识以及列类型语法。

<div id="quick-decision">
  ## 快速决策
</div>

* **如果**每个字段都有已知且稳定的类型，并且 schema 很少变动
  **→** [类型化列](#typed-columns)
* **如果**大多数字段都是稳定的，但某一部分是动态的或不可预测的
  **→** [混合方案 (类型化列 + JSON) ](#hybrid)
* **如果**整个结构都是动态的，并且键会在不同记录之间出现或消失
  **→** [原生 JSON 列](#native-json)
* **如果**动态字段是键值对，且值类型一致 (例如字符串标签、数值指标)
  **→** 选择 [`Map`](#when-map-fits-better) 而不是 JSON
* **如果**你只是存储和读取 JSON blob，而不进行字段级查询
  **→** [不透明 String 存储](#opaque-storage)

<Note>
  不要混淆 JSON *format* 和 JSON *column type*。你可以将 JSON 格式的数据 (通过 `JSONEachRow` 等) 插入到类型化列中，而完全不使用 `JSON` 列类型。这里要做的选择是列类型，而不是输入格式。
</Note>

<div id="approach-details">
  ## 方法说明
</div>

<div id="typed-columns">
  ### 类型化列
</div>

**适用场景：** JSON 结构在设计阶段已完全明确。各条记录之间的字段和类型保持不变。即使是复杂的嵌套结构 (如对象数组、嵌套 Map) ，也可以用 [`Array`](/docs/zh/reference/data-types/array)、[`Tuple`](/docs/zh/reference/data-types/tuple) 和 [`Nested`](/docs/zh/reference/data-types/nested-data-structures/index) 类型来表示。

**权衡取舍：** schema 变更需要执行 `ALTER TABLE`。如果不更新 schema，插入时出现的额外字段会被静默丢弃。

<Accordion title="设置、验证与注意事项">
  **设置**

  ```sql theme={null}
  CREATE TABLE events
  (
      `timestamp` DateTime,
      `service`   LowCardinality(String),
      `level`     Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
      `message`   String,
      `host`      LowCardinality(String),
      `duration_ms` UInt32
  )
  ENGINE = MergeTree
  ORDER BY (service, timestamp)
  ```

  **验证**

  ```sql theme={null}
  -- 确认列类型符合预期
  DESCRIBE TABLE events FORMAT Vertical

  -- 通过插入和查询来验证 schema 能否处理你的数据
  INSERT INTO events FORMAT JSONEachRow
  {"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42}

  SELECT service, level, duration_ms FROM events WHERE service = 'api'
  ```

  **注意事项**

  * 如果你使用 `JSONEachRow` 插入 JSON 数据，而 JSON 中包含 schema 里不存在的字段，ClickHouse 默认会静默丢弃这些字段。如果你希望改为报错，请将 [`input_format_skip_unknown_fields`](/docs/zh/reference/settings/formats#input_format_skip_unknown_fields) 设置为 `0`。
</Accordion>

***

<div id="hybrid">
  ### 混合方案 (类型化列 + JSON)
</div>

**适用场景：** 一组核心字段是稳定的 (时间戳、ID、状态码) ，但部分载荷是动态的。比如用户自定义属性、标签、元数据或扩展字段，这些字段会因记录而异。

**权衡：** 类型化列可获得完整性能，JSON 列则提供灵活性。不过，JSON 列中的动态部分仍会带来插入开销和存储成本。

<Accordion title="设置、验证与注意事项">
  **设置**

  ```sql theme={null}
  CREATE TABLE events
  (
      `timestamp`  DateTime,
      `service`    LowCardinality(String),
      `level`      Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
      `message`    String,
      `host`       LowCardinality(String),
      `duration_ms` UInt32,
      `attributes` JSON(
          max_dynamic_paths = 256,
          `http.status_code` UInt16,
          `http.method` LowCardinality(String),
          SKIP REGEXP 'debug\..*'
      )
  )
  ENGINE = MergeTree
  ORDER BY (service, timestamp)
  ```

  **验证**

  ```sql theme={null}
  -- 插入样本数据并查看推断出的路径
  INSERT INTO events FORMAT JSONEachRow
  {"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42,"attributes":{"http.status_code":200,"http.method":"GET","user.region":"eu-west","custom_tag":"abc"}}

  SELECT JSONAllPathsWithTypes(attributes)
  FROM events
  FORMAT PrettyJSONEachRow
  ```

  **注意事项**

  * 对于你预先已知的 JSON 路径，请使用 [类型提示](/docs/zh/reference/data-types/newjson)。类型提示会绕过判别列，并将该路径像普通类型化列一样存储，具有相同性能且没有额外开销。
  * 对于你永远不会查询的路径 (如调试元数据、内部追踪 ID) ，使用 `SKIP` 或 `SKIP REGEXP` 可以节省存储并减少子列数量。
  * 将 `max_dynamic_paths` 设置为与你实际查询的不同路径数量相匹配。默认值 (1024) 适用于大多数场景。如果动态部分较少，可以适当调低。
  * 不要将 `max_dynamic_paths` 设为高于 10,000。较高的值会增加资源消耗并降低效率。

  <Info>
    **带点号的键**

    默认情况下，带点号的键 (例如 `http.status_code`) 会被视为嵌套路径，因此 `{"http.status_code": 200}` 的存储方式与 `{"http": {"status_code": 200}}` 相同。这在 OTel 属性中很常见。可以使用类型提示来控制带点路径的存储方式，或者启用 `json_type_escape_dots_in_keys` (25.8+) 。
  </Info>
</Accordion>

***

<div id="native-json">
  ### 原生 JSON 列
</div>

**适用场景：** 数据结构确实难以预测，不同记录中的键可能会不断出现或消失。适合用于用户生成的 schema、插件系统，或无法控制上游 schema 的数据湖摄取场景。

**权衡取舍：** 插入速度比类型化列慢。读取整个对象也比 String 慢。子列管理还会带来额外的存储开销。但对于针对特定路径的字段级查询，这种方式表现良好。

<Accordion title="设置、验证和注意事项">
  **设置**

  ```sql theme={null}
  CREATE TABLE dynamic_events
  (
      `id`   UInt64,
      `ts`   DateTime DEFAULT now(),
      `data` JSON(
          max_dynamic_paths = 512,
          `event_type` LowCardinality(String),
          `version` UInt8
      )
  )
  ENGINE = MergeTree
  ORDER BY (data.event_type, ts)
  ```

  将完整的 JSON 文档插入 JSON 列时，请使用 [`JSONAsObject`](/docs/zh/reference/formats/JSON/JSONAsObject) 格式。它会将每一行输入视为一个完整的 JSON 对象，并将其映射到该列。

  **验证**

  ```sql theme={null}
  INSERT INTO dynamic_events (id, data) FORMAT JSONEachRow
  {"id": 1, "data": {"event_type": "click", "version": 2, "page": "/home", "button_id": "cta-1"}}
  {"id": 2, "data": {"event_type": "purchase", "version": 1, "item_id": "SKU-99", "amount": 49.99, "currency": "USD"}}

  -- 检查 ClickHouse 检测到的路径及其类型
  SELECT JSONAllPathsWithTypes(data) FROM dynamic_events FORMAT PrettyJSONEachRow

  -- 查询特定路径
  SELECT data.page FROM dynamic_events WHERE data.event_type = 'click'
  ```

  **注意事项**

  * 如果没有类型提示，ClickHouse 会根据每个路径最先看到的值来推断类型。如果某条记录中的 `score` 是 `"10"` (字符串) ，而另一条记录中是 `10` (整数) ，该路径就会生成一个判别列，查询也会变慢。对于类型已知的路径，建议添加提示。
  * 当路径数量超过 `max_dynamic_paths` 时，溢出值会移入[共享数据结构](/docs/zh/reference/data-types/newjson#shared-data-structure)，从而导致查询性能下降。可使用 [`JSONDynamicPaths()`](/docs/zh/reference/data-types/newjson#introspection-functions) 进行监控，并将该限制控制在 10,000 以下。
  * 每个动态路径最多支持 `max_dynamic_types` (默认值为 32) 种不同的数据类型。如果单个路径超过这个限制，额外类型会回退到共享的 Variant 存储。除非同一字段在你的数据中存在非常不一致的类型，否则这种情况通常影响不大。
</Accordion>

***

<div id="opaque-storage">
  ### 不透明 String 存储
</div>

**适用场景：** JSON 文档以整体形式存储和读取，然后传给应用、归档，或转发到下游。在 ClickHouse 内不做字段级筛选或聚合。

**权衡：** 写入最快，schema 也最简单。但如果不在运行时解析 (`JSONExtract` 系列) ，就无法进行字段级查询；而这种做法在大规模场景下会比较慢。

<Accordion title="设置、验证和注意事项">
  **设置**

  ```sql theme={null}
  CREATE TABLE raw_events
  (
      `id`        UInt64,
      `received`  DateTime DEFAULT now(),
      `payload`   String
  )
  ENGINE = MergeTree
  ORDER BY (received)
  ```

  **验证**

  ```sql theme={null}
  INSERT INTO raw_events (id, payload) VALUES
  (1, '{"type":"click","page":"/home"}'),
  (2, '{"type":"purchase","item":"SKU-99","amount":49.99}')

  -- 确认数据能够完整往返
  SELECT payload FROM raw_events WHERE id = 1

  -- 验证在需要时仍可临时解析字段
  SELECT JSONExtractString(payload, 'type') AS event_type FROM raw_events
  ```

  **注意事项**

  * 如果需求发生变化，后续需要字段级查询，就得使用类型化列或 JSON 列新建一张表，再对数据进行回填。如果你有任何可能需要查询单个字段的情况，建议一开始就改用[混合方案](#hybrid)。
  * `JSONExtract` 函数会在每次查询时解析字符串。用于临时探索还可以，但不适合生产环境中的仪表盘或高 QPS 工作负载。
  * 如果 JSON 载荷较大，可考虑对 String 列使用压缩编解码器 (`ZSTD`) ——它的压缩效果很好。
</Accordion>

<div id="comparison">
  ## 对比
</div>

| 维度             | 类型化列              | 混合方案                        | 原生 JSON                   | String     |
| -------------- | ----------------- | --------------------------- | ------------------------- | ---------- |
| **写入吞吐量**      | 最快                | 快                           | 中等                        | 最快         |
| **字段级查询**      | 最快                | 快 (类型化) ；良好 (带 hint 的 JSON) | 良好 (带 hint) ；较慢 (Dynamic) | 慢 (运行时解析)  |
| **整体对象读取**     | 快                 | 中等                          | 慢                         | 最快         |
| **存储效率**       | 最佳                | 良好                          | 中等                        | 良好 (压缩效果好) |
| **schema 灵活性** | 无 (`ALTER TABLE`) | 部分 (核心固定，尾部灵活)              | 完全                        | 完全         |
| **复杂度**        | 低                 | 中                           | 中等偏高                      | 低          |

<div id="when-map-fits-better">
  ## 何时更适合使用 Map
</div>

如果你的动态字段是同类型的键值对——也就是所有值都属于同一种类型——那么 [`Map(String, T)`](/docs/zh/reference/data-types/map) 会比 JSON 列更简单、更高效。常见示例包括：字符串标签 (`Map(String, String)`) 、数值指标 (`Map(String, Float64)`) 和功能开关 (`Map(String, Bool)`) 。

```sql theme={null}
CREATE TABLE tagged_events
(
    `timestamp` DateTime,
    `service`   LowCardinality(String),
    `tags`      Map(String, String)  -- e.g., {"env": "prod", "region": "us-east-1", "team": "platform"}
)
ENGINE = MergeTree
ORDER BY (service, timestamp)
```

`Map` 支持键级筛选 (`tags['env'] = 'prod'`) ，存储成本低于 JSON，并且避免了 JSON 类型的子列开销。请注意，默认情况下，按键查找会线性扫描整个 `Map`——对于较小的标签集这完全没问题，但如果 `Map` 包含 100+ 个键，建议考虑使用[`with_buckets` serialization](/docs/zh/reference/data-types/map#bucketed-map-serialization)。当值包含混合类型或结构存在嵌套时，请使用 JSON；当数据是扁平的键值对且值类型统一时，请使用 `Map`。

<div id="related-resources">
  ## 相关资源
</div>

* [在适当情况下使用 JSON](/docs/zh/concepts/best-practices/json-type) — 何时使用 JSON 列类型而非其他方案
* [JSON 数据类型参考](/docs/zh/reference/data-types/newjson) — 类型提示、SKIP、max\_dynamic\_paths 和内部信息函数的完整语法
* [选择数据类型](/docs/zh/concepts/best-practices/select-data-type) — 数据类型选择的一般指导
* [A New Powerful JSON Data Type for ClickHouse](https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse) — 深入解析 JSON 类型的存储架构
* [JSON 格式参考](/docs/zh/reference/formats/JSON/JSON) — JSON 数据的输入/输出格式 (JSONEachRow、JSONAsObject 等)
