Spark 4.0 的 VARIANT 类型是什么?半结构化 JSON 怎么存查得更快?

第 1271 题 约 3 分钟读完

本文目录
  1. 面试怎么回答
  2. 半结构化数据以前的三种存法
  3. VARIANT 是什么
  4. Shredding:让常用字段像普通列一样快
  5. 怎么选
  6. 使用注意

面试怎么回答

以前 JSON 要么存 STRING,每次查询都要从头解析;要么展开成 STRUCT,查询快但 Schema 要事先定好,上游一改就要改表。Spark 4.0 的 VARIANT 把 JSON 解析一次存成自描述的二进制格式,保留原始类型,查询用 variant_get 按路径取值,可以直接跳到目标字段,不用重新解析文本,又不需要事先定义 Schema。再进一步是 Shredding,把高频且类型稳定的字段拆成独立的 Parquet 列,能做列裁剪和谓词下推。实际用法是核心字段建普通列,多变的扩展属性放 VARIANT;跨引擎使用前要确认版本支持,高频字段最终还是要沉淀成标准列。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

埋点日志、接口返回、配置数据,大量都是 JSON。以前数仓里最常见的做法是存成 STRING,查询时用 get_json_object 解析,又慢又难治理。Spark 4.0 正式引入了 VARIANT 类型,Delta、Iceberg V3 和 Parquet 规范也在跟进。面试官问这题,想看你对半结构化数据的存储方案有没有思考。

半结构化数据以前的三种存法

  1. 存成 STRING:写入最简单,但每次查询都要从头解析整段文本,取一个字段也要解析全部;没有类型,数值比较容易出错;
  2. 展开成 STRUCT / 固定列:查询快、能列裁剪,但 Schema 要事先定好,上游一加字段就要改表,字段多了表结构爆炸;
  3. MAP<STRING, STRING>:灵活,但值全变成字符串,嵌套结构表达不了。

VARIANT 是什么

VARIANT 是一种自描述的二进制编码:把 JSON 解析一次,存成「元数据(字段名字典)+ 值(带类型的二进制)」的格式。

  • 写入时用 parse_json(json_str) 转成 VARIANT,解析只做一次;
  • 查询时用 variant_get(v, '$.user.id', 'bigint') 按路径取值并转换类型,不合法时可以用 try_variant_get 返回 NULL;
  • 保留了原始类型(整数、小数、字符串、布尔、数组、对象),不需要事先定义 Schema;
  • 二进制格式可以按偏移直接跳到目标字段,不用像文本 JSON 那样从头扫描。

Shredding:让常用字段像普通列一样快

光有二进制编码还不够,取一个字段仍然要读整个 VARIANT 值。Shredding(拆分)的思路是:把高频、类型稳定的字段额外拆成独立的 Parquet 列存放,其余字段留在 VARIANT 里。

  • 查询常用字段时只读这一列,可以列裁剪,还能用上 Parquet 的 min/max 统计做谓词下推;
  • 不常用的字段依然保留灵活性。

Parquet 规范已经定义了 Variant 和 Shredding 的编码,各引擎在陆续支持,用之前要确认你所用版本对 Shredding 读写的支持程度。

怎么选

  • 字段稳定、查询频繁:直接建成普通列,最快最好治理;
  • 字段多变、来源多样(埋点属性、第三方回调):用 VARIANT,比 STRING 快、比 STRUCT 灵活;
  • 只是原样存档、几乎不查:STRING 也可以;
  • 常见的组合:核心字段(事件名、用户 ID、时间)建普通列,扩展属性放 VARIANT。

使用注意

  • 跨引擎读写要确认表格式和引擎版本都支持 VARIANT;
  • 在 VARIANT 上做过滤和 Join 前,先用 variant_get 显式转成目标类型;
  • 不要把它当成逃避建模的理由,高频指标字段还是要沉淀成标准列。