Skip to main content
JSON 列类型从 ClickHouse 25.3+ 开始已可用于生产环境。不建议在生产环境中使用更早的版本。
如果你的数据以 JSON 形式写入,ClickHouse 提供了多种存储方式,从完全类型化的列到原始 String。具体选择取决于你的 schema 结构有多稳定,以及你是否需要字段级查询。 范围: 本页介绍存储 JSON 数据时的 schema 设计决策。不涵盖 JSON 输入/输出格式JSON 函数 或查询语法。有关 JSON 列类型本身的更多背景信息,请参见 在适当情况下使用 JSON 前提: 你应熟悉 ClickHouse 表创建MergeTree 的基础知识以及列类型语法。

快速决策

  • 如果每个字段都有已知且稳定的类型,并且 schema 很少变动 类型化列
  • 如果大多数字段都是稳定的,但某一部分是动态的或不可预测的 混合方案 (类型化列 + JSON)
  • 如果整个结构都是动态的,并且键会在不同记录之间出现或消失 原生 JSON 列
  • 如果动态字段是键值对,且值类型一致 (例如字符串标签、数值指标) 选择 Map 而不是 JSON
  • 如果你只是存储和读取 JSON blob,而不进行字段级查询 不透明 String 存储
不要混淆 JSON format 和 JSON column type。你可以将 JSON 格式的数据 (通过 JSONEachRow 等) 插入到类型化列中,而完全不使用 JSON 列类型。这里要做的选择是列类型,而不是输入格式。

方法说明

类型化列

适用场景: JSON 结构在设计阶段已完全明确。各条记录之间的字段和类型保持不变。即使是复杂的嵌套结构 (如对象数组、嵌套 Map) ,也可以用 ArrayTupleNested 类型来表示。 权衡取舍: schema 变更需要执行 ALTER TABLE。如果不更新 schema,插入时出现的额外字段会被静默丢弃。
设置
验证
注意事项
  • 如果你使用 JSONEachRow 插入 JSON 数据,而 JSON 中包含 schema 里不存在的字段,ClickHouse 默认会静默丢弃这些字段。如果你希望改为报错,请将 input_format_skip_unknown_fields 设置为 0

混合方案 (类型化列 + JSON)

适用场景: 一组核心字段是稳定的 (时间戳、ID、状态码) ,但部分载荷是动态的。比如用户自定义属性、标签、元数据或扩展字段,这些字段会因记录而异。 权衡: 类型化列可获得完整性能,JSON 列则提供灵活性。不过,JSON 列中的动态部分仍会带来插入开销和存储成本。
设置
验证
注意事项
  • 对于你预先已知的 JSON 路径,请使用 类型提示。类型提示会绕过判别列,并将该路径像普通类型化列一样存储,具有相同性能且没有额外开销。
  • 对于你永远不会查询的路径 (如调试元数据、内部追踪 ID) ,使用 SKIPSKIP REGEXP 可以节省存储并减少子列数量。
  • max_dynamic_paths 设置为与你实际查询的不同路径数量相匹配。默认值 (1024) 适用于大多数场景。如果动态部分较少,可以适当调低。
  • 不要将 max_dynamic_paths 设为高于 10,000。较高的值会增加资源消耗并降低效率。
带点号的键默认情况下,带点号的键 (例如 http.status_code) 会被视为嵌套路径,因此 {"http.status_code": 200} 的存储方式与 {"http": {"status_code": 200}} 相同。这在 OTel 属性中很常见。可以使用类型提示来控制带点路径的存储方式,或者启用 json_type_escape_dots_in_keys (25.8+) 。

原生 JSON 列

适用场景: 数据结构确实难以预测,不同记录中的键可能会不断出现或消失。适合用于用户生成的 schema、插件系统,或无法控制上游 schema 的数据湖摄取场景。 权衡取舍: 插入速度比类型化列慢。读取整个对象也比 String 慢。子列管理还会带来额外的存储开销。但对于针对特定路径的字段级查询,这种方式表现良好。
设置
将完整的 JSON 文档插入 JSON 列时,请使用 JSONAsObject 格式。它会将每一行输入视为一个完整的 JSON 对象,并将其映射到该列。验证
注意事项
  • 如果没有类型提示,ClickHouse 会根据每个路径最先看到的值来推断类型。如果某条记录中的 score"10" (字符串) ,而另一条记录中是 10 (整数) ,该路径就会生成一个判别列,查询也会变慢。对于类型已知的路径,建议添加提示。
  • 当路径数量超过 max_dynamic_paths 时,溢出值会移入共享数据结构,从而导致查询性能下降。可使用 JSONDynamicPaths() 进行监控,并将该限制控制在 10,000 以下。
  • 每个动态路径最多支持 max_dynamic_types (默认值为 32) 种不同的数据类型。如果单个路径超过这个限制,额外类型会回退到共享的 Variant 存储。除非同一字段在你的数据中存在非常不一致的类型,否则这种情况通常影响不大。

不透明 String 存储

适用场景: JSON 文档以整体形式存储和读取,然后传给应用、归档,或转发到下游。在 ClickHouse 内不做字段级筛选或聚合。 权衡: 写入最快,schema 也最简单。但如果不在运行时解析 (JSONExtract 系列) ,就无法进行字段级查询;而这种做法在大规模场景下会比较慢。
设置
验证
注意事项
  • 如果需求发生变化,后续需要字段级查询,就得使用类型化列或 JSON 列新建一张表,再对数据进行回填。如果你有任何可能需要查询单个字段的情况,建议一开始就改用混合方案
  • JSONExtract 函数会在每次查询时解析字符串。用于临时探索还可以,但不适合生产环境中的仪表盘或高 QPS 工作负载。
  • 如果 JSON 载荷较大,可考虑对 String 列使用压缩编解码器 (ZSTD) ——它的压缩效果很好。

对比

何时更适合使用 Map

如果你的动态字段是同类型的键值对——也就是所有值都属于同一种类型——那么 Map(String, T) 会比 JSON 列更简单、更高效。常见示例包括:字符串标签 (Map(String, String)) 、数值指标 (Map(String, Float64)) 和功能开关 (Map(String, Bool)) 。
Map 支持键级筛选 (tags['env'] = 'prod') ,存储成本低于 JSON,并且避免了 JSON 类型的子列开销。请注意,默认情况下,按键查找会线性扫描整个 Map——对于较小的标签集这完全没问题,但如果 Map 包含 100+ 个键,建议考虑使用with_buckets serialization。当值包含混合类型或结构存在嵌套时,请使用 JSON;当数据是扁平的键值对且值类型统一时,请使用 Map
最后修改于 2026年7月23日