在合适的场景下使用 JSON
ClickHouse 现在提供了适用于半结构化和动态数据的原生 JSON 列类型。需要特别说明的是,这是一种列类型,而不是一种数据格式——可以以字符串形式将 JSON 插入 ClickHouse,或者通过 JSONEachRow 等受支持的格式进行插入,但这并不意味着就在使用 JSON 列类型。只有在数据结构本身是动态的情况下,才应该选择 JSON 类型,而不是因为“刚好”以 JSON 形式存储数据就使用它。
何时使用 JSON 类型
JSON 类型专为在结构动态或不可预测的 JSON 对象中,对特定字段进行查询、过滤和聚合而设计。它通过将 JSON 对象拆分为多个子列来实现这一点,与使用 Map 或解析字符串等方案相比,可以显著减少选定字段的数据读取量,从而加速查询。
不过,这也带来了一些重要的权衡:
INSERT操作较慢 —— 将 JSON 拆分为子列、执行类型推断以及管理灵活的存储结构,会使插入操作比将 JSON 作为简单的String列存储更慢。- 读取整个对象时较慢 —— 如果你需要获取完整的 JSON 文档(而不是特定字段),从
JSON类型读取会比从String列读取更慢。当你不进行字段级查询时,从多个子列重建对象的开销并不会带来任何收益。 - 存储开销更大 —— 相比将 JSON 存为单个字符串值,维护独立的子列会增加结构性存储开销。
在以下情况使用 JSON 类型:
- 当数据结构具有动态性或不可预测性,不同文档之间的键各不相同
- 字段类型或模式(schema)会随时间变化,或在不同记录之间有所差异
- 你需要在无法预先预测结构的 JSON 对象中,对特定路径进行查询、过滤或聚合
- 你的使用场景涉及半结构化数据,如日志、事件,或模式(schema)不一致的用户生成内容
在以下情况下使用 String 列(或结构化类型):
- 你的数据结构是已知且保持一致的——在这种情况下,应使用常规列、
Tuple、Array、Dynamic或Variant类型来代替 JSON文档被当作不透明的二进制大对象(blob),只以整体形式存储和检索,而不进行字段级分析- 你不需要在数据库中对单个 JSON 字段进行查询或过滤
JSON只是传输/存储格式,不在 ClickHouse 内进行分析
如果 JSON 是在数据库内部不被分析、仅用于存储和读取的不透明文档,则应将其存储为 String 字段。只有当你需要在动态 JSON 结构中的特定字段上进行高效的查询、过滤或聚合时,JSON 类型的优势才会体现出来。
你也可以组合使用多种方式——对可预测的顶层字段使用标准列,对负载中动态部分使用 JSON 列。
使用 JSON 的注意事项和技巧
JSON 类型通过将路径扁平化为子列,实现高效的列式存储。但灵活性也意味着更高的使用要求。要高效使用它,请:
- 为路径指定类型,在列定义中使用类型提示为已知子列指定类型,从而避免不必要的类型推断。
- 跳过不需要的路径,使用 SKIP 和 SKIP REGEXP 来减少存储并提升性能。
- 避免将
max_dynamic_paths设置得过高——过大的值会增加资源消耗并降低效率。经验法则是将其保持在 10,000 以下。
类型提示不仅仅是避免不必要类型推断的一种方式——它们还能完全消除存储和处理中的间接层。带有类型提示的 JSON 路径始终与传统列以相同方式存储,从而无需在查询时依赖判别列(discriminator column)或进行动态解析。这意味着,在合理定义类型提示的情况下,嵌套 JSON 字段可以获得与从一开始就被建模为顶层字段相同的性能和效率。因此,对于大部分结构一致、但仍希望利用 JSON 灵活性的数据集,类型提示提供了一种便捷方式,可以在无需重构 schema 或摄取管道的前提下保持性能。
高级特性
- JSON 列 可以和其他列一样用作主键。不能为子列指定编解码器(codec)。
- 它们支持通过诸如
JSONAllPathsWithTypes()和JSONDynamicPaths()等函数进行自省。 - 可以使用
.^语法读取嵌套子对象。 - 查询语法可能与标准 SQL 不同,对于嵌套字段可能需要进行特殊的类型转换或使用特定运算符。
如需更多指导,请参阅 ClickHouse JSON 文档 或查看我们的博文 A New Powerful JSON Data Type for ClickHouse。
示例
考虑如下 JSON 示例,它表示 Python PyPI 数据集 中的一行数据:
假设该 schema 是静态的,并且各个类型都可以被很好地定义。即使数据是 NDJSON 格式(每行一条 JSON 数据),对于这样一个 schema 也没有必要使用 JSON 类型。只需使用常规数据类型来定义该 schema 即可。
并插入 JSON 行数据:
考虑包含 250 万篇学术论文的 arXiv 数据集。该数据集以 NDJSON 格式分发,其中的每一行都代表一篇已发表的学术论文。示例行如下所示:
尽管此处的 JSON 结构较为复杂并包含嵌套,但它是可预测的,字段的数量和类型不会变化。在本示例中,我们既可以使用 JSON 类型,也可以直接使用 Tuples 和 Nested 类型显式地定义该结构:
同样可以将这些数据作为 JSON 插入:
假设又新增了一列名为 tags。如果它只是一个字符串列表,我们可以将其建模为 Array(String),但这里假设你可以添加具有混合类型的任意标签结构(注意 score 既可以是字符串也可以是整数)。我们修改后的 JSON 文档如下:
在本例中,我们可以将 arXiv 文档全部建模为 JSON,或者只添加一个 JSON 类型的 tags 列。下面我们提供这两种示例:
我们在 JSON 定义中为 update_date 列提供了类型提示,因为我们在排序键/主键中使用了它。这有助于让 ClickHouse 确定该列不会为 null,并确保它能够知道应使用哪个 update_date 子列(每种类型可能都有多个子列,否则会产生歧义)。
我们可以向此表插入数据,并使用 JSONAllPathsWithTypes 函数和 PrettyJSONEachRow 输出格式查看之后推断出的表结构:
或者,我们也可以使用之前的 schema,并加上一列 JSON 类型的 tags 来进行建模。通常更推荐这种方式,因为这样可以最大限度减少 ClickHouse 所需的推断:
我们现在可以推断出子列 tags 的数据类型。