静态 JSON 与动态 JSON
- 基本类型 - 如果键的值是基本类型,无论它位于子对象中还是根级,都应根据通用 schema 设计最佳实践 和 类型优化规则 选择其类型。基本类型数组 (如下方的
phone_numbers) 可以建模为Array(<type>),例如Array(String)。 - 静态 vs 动态 - 如果键的值是复杂对象,即对象或对象数组,请判断它是否会发生变化。对于很少出现新键、且新增键可以预见并通过
ALTER TABLE ADD COLUMN进行 schema 变更处理的对象,可视为静态。这也包括这样的对象:某些 JSON 文档中可能只包含其中一部分键。那些经常新增键和/或新增键不可预测的对象,则应视为动态。这里有一个例外:对于包含数百或数千个子键的结构,出于便利性考虑,也可以将其视为动态。
- 根键
name、username、email、website可以用String类型表示。列phone_numbers是一个类型为Array(String)的 Array 基本类型,而dob和id的类型则分别为Date和UInt32。 - 不会向
address对象添加新键 (只会添加新的地址对象) ,因此它可以视为静态。如果继续递归展开,除geo之外,所有子列都可以视为基本类型 (且类型为String) 。geo也是一个静态结构,包含两个Float32列:lat和lon。 tags列是动态的。我们假设可以向该对象添加任意类型、任意结构的新标签。company对象是静态的,并且始终最多只包含指定的 3 个键。子键name和catchPhrase的类型为String。键labels是动态的。我们假设可以向该对象添加新的任意标签。其值始终为String类型的键值对。
包含数百或数千个静态键的结构也可视为动态结构,因为实际上很少会为这类结构静态声明列。不过,在可能的情况下,请跳过不需要的路径,以同时节省存储空间并降低推断开销。
处理静态结构
Tuple,来处理静态结构。对象数组可以用元组数组,即 Array(Tuple),来表示。在元组内部,列及其对应的类型也应按相同规则定义。这样一来,就可以像下面所示那样,使用嵌套的 Tuple 来表示嵌套对象。
为了说明这一点,我们沿用前面的 JSON person 示例,并省略动态对象:
company 列定义为 Tuple(catchPhrase String, name String)。address 键使用 Array(Tuple),并通过一个嵌套的 Tuple 表示 geo 列。
JSON 可以按当前结构插入此表:
address.street 列会以 Array 的形式返回。要按位置查询数组中的特定对象,应在列名后指定数组偏移量。例如,要访问第一个地址中的 street:
24.12:
处理默认值
Tuple 类型并不要求 JSON 载荷中包含所有列。若未提供,则会使用默认值。
以前面的 people 表和下面这段稀疏 JSON 为例,其中缺少 suite、geo、phone_numbers 和 catchPhrase 这些键。
处理新增列
nickname 键:
nickname 键:
ALTER TABLE ADD COLUMN 命令向 schema 添加列。可以通过 DEFAULT 子句指定默认值;如果在后续插入操作中未指定该值,则会使用此默认值。对于那些没有该值的行 (因为它们是在该列创建之前插入的) ,也会返回此默认值。如果未指定 DEFAULT 值,则会使用该类型的默认值。
例如:
处理半结构化/动态结构
JSON 类型。
更具体地说,在以下情况下,建议使用 JSON 类型:
- 存在不可预测的键,并且这些键会随时间变化。
- 包含类型会变化的值 (例如,某个 路径 有时可能是字符串,有时可能是数值) 。
- 需要具备灵活的 schema,而严格类型约束并不适用。
- 你有数百甚至数千个静态 路径,但逐一显式声明并不现实。这种情况通常较少见。
company.labels 对象被认定为动态的。
假设 company.labels 包含任意键。此外,该结构中任意键的类型在不同行之间也可能不一致。例如:
company.labels 列的键和类型是动态变化的,我们有几种方式来对这些数据进行建模:
- 单个 JSON 列 - 将整个 schema 表示为单个
JSON列,使其下的所有结构都可以动态变化。 - 定向 JSON 列 - 仅对
company.labels列使用JSON类型,同时对其他所有列保留上述结构化 schema。
- 数据验证 – 强制采用严格的 schema,可避免特定结构之外出现列数爆炸的风险。
- 避免列数爆炸风险 - 尽管 JSON 类型可扩展到潜在的数千列,其中 subcolumns 会作为独立列存储,但这可能导致列文件数量激增,即创建过多列文件,从而影响性能。为缓解这一问题,JSON 所使用的底层 Dynamic 类型 提供了
max_dynamic_paths参数,用于限制以独立列文件形式存储的唯一路径数量。一旦达到该阈值,额外路径会以紧凑编码格式存储到共享列文件中,从而在支持灵活数据摄取的同时兼顾性能和存储效率。不过,访问该共享列文件时,性能会有所下降。不过还要注意,JSON 列也可以结合类型提示使用。带有“提示”的列可提供与独立列相同的性能。 - 更便于查看路径和类型的内部信息 - 尽管 JSON 类型支持使用内部信息函数来确定已推断出的类型和路径,但静态结构通常更容易查看,例如使用
DESCRIBE。
单个 JSON 列
JSON 用于动态子结构。
性能注意事项单个 JSON 列可通过跳过 (不存储) 不需要的 JSON 路径,以及使用类型提示进行优化。类型提示允许用户为子列显式定义类型,从而跳过查询时的类型推断和间接处理。这样可以达到与使用显式 schema 相同的性能。更多详情,请参见“使用类型提示和跳过路径”。
由于我们会在排序键/主键中使用
username 列,因此在 JSON 定义中为其提供了一个类型提示。这有助于 ClickHouse 确认该列不会为 NULL,并明确应使用哪个 username 子列 (每种类型都可能对应多个子列,否则这里会有歧义) 。JSONAsObject 格式将行插入上述表中:
. 记法访问,例如
NULL。
此外,对于类型相同的路径,还会单独创建一个子列。例如,company.labels.type 既可能是 String,也可能是 Array(Nullable(String)),因此会分别存在对应的子列。虽然在可能的情况下两者都会被返回,但我们也可以使用 .: 语法来指定特定的子列:
^。这是有意的设计选择,目的是避免读取过多列,除非显式请求。如下所示,不带 ^ 访问的对象将返回 NULL:
目标 JSON 列
JSON 列来建模 company.labels 列。
JSONEachRow 格式向该表插入数据:
company.labels 列的推断路径和类型。
使用类型提示和跳过路径
company.labels 中的 JSON 键 dissolved、employees 和 founded 指定了类型
SKIP 和 SKIP REGEXP 参数,跳过 JSON 中不希望存储的路径,以尽可能减少存储开销,并避免对不需要的路径进行多余的推断。例如,假设我们为上述数据使用单个 JSON 列,则可以跳过 address 和 company 路径:
使用类型提示优化性能
配置动态路径
max_dynamic_paths 参数控制。
SKIP 参数来限制存储内容。
如果你想了解这种新列类型的实现细节,我们建议阅读我们的详细博文 “ClickHouse 的全新强大 JSON 数据类型”。