bigywater.tech
ClickHouse 内部原理 · 50

JSON 类型和 JSONExtract 函数

真实插入两条结构不同的JSON文档到原生JSON类型列,验证字段查询的便利性,并用columns_substreams.txt揭示背后真实惊人的物理存储复杂度。

47个substream
真实1个JSON列、2行数据就产生的物理文件数
两层存储
真实常见路径独立类型化,长尾路径共享兜底

做的事:真实插入两条结构不同的JSON文档

代码CREATE TABLE demo.jsontest (k UInt64, doc JSON) ENGINE=MergeTree ORDER BY k; INSERT INTO demo.jsontest VALUES (1, '{"name":"alice","age":30,"tags":["a","b"]}'); INSERT INTO demo.jsontest VALUES (2, '{"name":"bob","score":9.5}'); -- 结构完全不同 SELECT k, doc.name, doc.age, doc.score FROM demo.jsontest ORDER BY k;

验证:字段真实能直接当成普通列一样查询

实测 · 本机执行输出
SELECT k, doc.name, doc.age, doc.score FROM demo.jsontest ORDER BY k;
→ 1   alice   30    \N     ← 第1行没有score字段,真实自动是NULL
  2   bob     \N    9.5    ← 第2行没有age字段,真实自动是NULL

不需要写JSONExtract,doc.name/doc.age这种点号路径真实就能直接当成表的普通列来查询——缺失的字段真实自动补NULL,不需要额外处理。

诚实的复杂度:1个JSON列,真实拆成了47个物理文件

实测 · 本机执行输出(columns_substreams.txt,节选)
47 substreams for column `doc`:
	doc.object_structure
	doc.age.dynamic_structure      doc.age.variant_discr_prefix
	doc.age.variant_discr          doc.age.Int64
	doc.age.SharedVariant.size     doc.age.SharedVariant
	doc.name.dynamic_structure     doc.name.variant_discr_prefix
	doc.name.variant_discr         doc.name.String.size   doc.name.String
	doc.tags.Array%28Nullable%28String%29%29.size0  ...
	doc.object_shared_data.0.size0  doc.object_shared_data.0%2Epaths  doc.object_shared_data.0%2Evalues
	doc.object_shared_data.1.size0  ...(一直到 .7,共8组)
这份真实的清单揭示了原生JSON类型内部真实的复杂度:本文的name、age、tags这三个在样本数据里出现过的字段,真实都拿到了各自专属的"动态类型"存储——每个字段除了真实的数据本身(比如doc.age.Int64),还配了一个variant_discr(variant判别符)子流,用来标记"这一行这个字段实际存的是什么类型",因为JSON字段的类型本来就可能逐行不同(这一行age是数字,另一行万一是字符串)。而没有被识别为"常见路径"的字段,真实没有各自单独开一列,而是被归进了object_shared_data这组"共享数据区"——本文真实看到有0到7号一共8个桶,每个桶都是一份通用的path-value键值对存储,用来兜底那些不值得单独开一整套动态类型存储的稀疏字段。这解释了原生JSON类型真实想解决的问题:既要让"常见的、几乎每行都有"的字段享受到接近普通列(各自独立压缩、独立索引)的读取效率,又要让"偶尔出现的、路径五花八门"的长尾字段不至于让表结构爆炸成成百上千个物理列——用一个"常见路径独立存储 + 长尾路径共享存储"的两层结构来折中。代价也很直接:哪怕本文只插入了2行、涉及4个不同路径,物理上就已经真实产生了47个substream,这个复杂度远超本系列前面验证过的Array/Tuple/Map,值得在决定要不要用原生JSON类型存储高度动态的数据前,心里有数。

分步演示:从简单的字段查询到47个substream的真实复杂度

每一步都是本文上面用真实原生JSON类型跑出来的原始结果。

1个JSON列,真实拆成47个物理文件
这一步新验证的结构

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实的原生 JSON 类型(非String+JSONExtract)
    • 真实的 columns_substreams.txt 揭示JSON列的物理拆分结构
    京ICP备2026067502号-1京公网安备11010502063487号