Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

运行手册:JSON schema

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

范围: 本页介绍存储 JSON 数据时的 schema 设计决策。不涵盖 JSON 输入/输出格式JSON 函数 或查询语法。有关 JSON 列类型本身的更多背景信息,请参见 在适当情况下使用 JSON

前提: 你应熟悉 ClickHouse 表创建MergeTree 的基础知识以及列类型语法。

快速决策

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

方法说明

类型化列

适用场景: JSON 结构在设计阶段已完全明确。各条记录之间的字段和类型保持不变。即使是复杂的嵌套结构 (如对象数组、嵌套 Map) ,也可以用 ArrayTupleNested 类型来表示。

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

设置、验证与注意事项

设置

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)

验证

-- 确认列类型符合预期
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 设置为 0

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

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

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

设置、验证与注意事项

设置

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)

验证

-- 插入样本数据并查看推断出的路径
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 路径,请使用 类型提示。类型提示会绕过判别列,并将该路径像普通类型化列一样存储,具有相同性能且没有额外开销。
  • 对于你永远不会查询的路径 (如调试元数据、内部追踪 ID) ,使用 SKIPSKIP REGEXP 可以节省存储并减少子列数量。
  • max_dynamic_paths 设置为与你实际查询的不同路径数量相匹配。默认值 (1024) 适用于大多数场景。如果动态部分较少,可以适当调低。
  • 不要将 max_dynamic_paths 设为高于 10,000。较高的值会增加资源消耗并降低效率。

原生 JSON 列

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

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

设置、验证和注意事项

设置

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 格式。它会将每一行输入视为一个完整的 JSON 对象,并将其映射到该列。

验证

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 时,溢出值会移入共享数据结构,从而导致查询性能下降。可使用 JSONDynamicPaths() 进行监控,并将该限制控制在 10,000 以下。
  • 每个动态路径最多支持 max_dynamic_types (默认值为 32) 种不同的数据类型。如果单个路径超过这个限制,额外类型会回退到共享的 Variant 存储。除非同一字段在你的数据中存在非常不一致的类型,否则这种情况通常影响不大。

不透明 String 存储

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

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

设置、验证和注意事项

设置

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

验证

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 列新建一张表,再对数据进行回填。如果你有任何可能需要查询单个字段的情况,建议一开始就改用混合方案
  • JSONExtract 函数会在每次查询时解析字符串。用于临时探索还可以,但不适合生产环境中的仪表盘或高 QPS 工作负载。
  • 如果 JSON 载荷较大,可考虑对 String 列使用压缩编解码器 (ZSTD) ——它的压缩效果很好。

对比

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

何时更适合使用 Map

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

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。当值包含混合类型或结构存在嵌套时,请使用 JSON;当数据是扁平的键值对且值类型统一时,请使用 Map

Navigation