Apache Doris VARIANT 类型学习笔记:子列化原理、四种存储模式与配置清单

这篇是给自己留档的笔记。起因是把一张埋点表从「整条 JSON 存 STRING」改成 Apache Doris 的 VARIANT 类型,前后需要确认的细节太多,散在文档各处,索性按「原理 → 类型规则 → 存储模式 → 配置清单 → 建表批注」的顺序整理成一页,方便以后回查。

结论先行:改动本身只涉及建表语句和查询里的路径访问写法,但真正决定效果的是子列化的边界管理——路径数、类型稳定性、宽列模式选择这三件事。

一、先说结论

官方 ClickBench 改造测试(43 条查询)给出的三组数字,是判断这次改造值不值得做的依据:

观测项 预定义静态列 VARIANT 类型 JSON 类型
存储空间 12.618 GB 12.718 GB 35.711 GB
第一次查询(冷) 233.79s 248.66s 大部分查询超时
第三次查询(热) 83.03s 92.29s 743.69s

适用边界,按笔记形式列清单:

  • 适合:字段集持续变化的 JSON 日志、埋点、设备上报;查询集中在少数热点路径
  • 适合:需要对某些路径做等值过滤或全文检索的场景
  • 不适合:schema 已稳定多年、核心字段要当 join key 或排序键的场合,静态列更合适
  • 不适合:主要需求是归档原始 JSON 而不是按路径分析,此时 VARIANT 的收益有限

一句话:VARIANT 相比 JSON 节省约 65% 存储、查询提速 8 倍以上,与预定义静态列在热查询上的差距约 10%。

二、原理:写入链路上发生了什么

JSON 类型的存储方式是二进制 JSONB 按行写入 segment 文件。查询某个路径时,存储层无法只定位到该路径,必须把整条文档解析出来。解析开销随嵌套深度和文档体积增长,这就是热查询 743.69s 的主要来源。

VARIANT 的做法是在写入侧做两件事:

一是类型推断与 schema 合并。 写入时按 JSON Path 推断子路径类型,动态合并到已有 schema 中。推断出的基础类型包括:TinyInt、NULL(等价于 JSON 的 null)、BigInt(64 位)、Double、String(Text)、Jsonb、Variant(嵌套对象)、Array<T>(仅支持一维)。

二是子列列式提取(Subcolumnization)。 高频路径被展开为独立的列式子列,按列式存储落盘,从而享受列式压缩、稀疏索引裁剪和向量化执行。后续写入出现新路径时自动合并,不需要改表。

类型不兼容时的提升规则按顺序是:整型按 tinyint → smallint → int → bigint 方向提升;浮点按 float → double 提升;再往上是 text / string,最后是 JSON。上述规则都不兼容时,统一转为 JSON 类型以避免类型信息丢失。这条规则有一个直接后果:同一路径类型冲突会被提升为 JSONB,该路径上的索引随之失效,这是笔记里要重点标红的一条。

日期、时间戳、Decimal 这类非标准 JSON 类型,未指定 schema 时会以字符串形式存储;需要较高计算效率就提取为静态列,或在 Schema Template 中显式声明类型。

三、配置与批注清单

3.1 Schema Template 能声明的类型

Schema Template 用于在建表时把若干路径的类型固定下来。除基础类型外,支持的扩展类型清单:

  • Number 扩展:Decimal32 / Decimal64 / Decimal128 / Decimal256、LargeInt
  • 时间:Datetime、Timestamptz、Date
  • 网络:IPV4、IPV6
  • 其他:Boolean、ARRAY<T>(T 为以上任意类型,仅支持一维)

批注三条:

  1. 预定义 Schema 只能在建表时指定,当前不支持通过 ALTER 修改。
  2. 支持前缀通配符,'message*' : STRING 表示前缀匹配 message 的子列统一为 STRING;同一路径命中多个 pattern 时,取定义顺序中第一个匹配的类型。
  3. 列名本身含 * 且要按名称精确匹配时,写 MATCH_NAME 'enumString*' : STRING。

类型推断可以用一组最小 INSERT 验证,这条片段建议留着做回归:

CREATE TABLE IF NOT EXISTS variant_tbl (
    k BIGINT,
    v VARIANT
) PROPERTIES("replication_num" = "1");

INSERT INTO variant_tbl VALUES
  (1, 'null'), (2, NULL), (3, 'true'), (4, '-17'),
  (5, '123.12'), (6, '1.912'), (7, '"A quote"'),
  (8, '[-1, 12, false]'), (9, '{ "x": "abc", "y": false, "z": 10 }'),
  (10, '"2021-01-01"');

-- 不存在的 JSON 键返回 NULL
SELECT v['a']['no_such_key'] FROM variant_tbl;

批注:'null' 与 SQL NULL 在 VARIANT 中等价;'[-1, 12, false]' 是一维数组,二维及以上数组会以 JSONB 编码存储;'"2021-01-01"' 这类非标准 JSON 类型在未指定 Schema 时以字符串形式存储,需要较高计算效率就在 Schema Template 里显式声明为 DATE 或 DATETIME。

3.2 四种存储模式对照表

模式 适用画像 关键配置 注意事项
默认 VARIANT 事件日志、审计日志,路径数可控 V3 存储格式,其余保持默认 不要提前调大 variant_max_subcolumns_count
稀疏列 广告 / 遥测 / 用户画像,key 总量大但热点集中 variant_max_subcolumns_count、variant_sparse_hash_shard_count JSON key 稀疏度接近时收益下降
DOC 编码 模型输出、trace 快照、归档文档,常整条取回 variant_enable_doc_mode、variant_doc_materialization_min_rows 与稀疏列互斥,二者不能同时启用
Schema Template 订单 / 支付 / 设备,少数关键路径需稳定类型 在列上声明路径类型,可配 field_pattern 索引 只覆盖关键路径,不要把整个文档写成模板

宽列场景的一个横向参考:官方在 1 万条路径的宽列数据集上(20 万行、抽取单个 key、16 核、三次取中位数)测得,VARIANT 默认模式与 DOC 已物化模式均为 76 ms、峰值内存 1 MiB,而原始 STRING 为 6104 ms、峰值内存 48 GiB。

3.3 配置参数清单

参数 / 属性 默认值 作用 备注
variant_max_subcolumns_count 2048 参与子列提取的路径数上限 0 表示不限制,实践上不建议超过 10000
variant_enable_typed_paths_to_sparse false 允许声明了类型的路径也计入阈值 3.1 及以上版本可用
variant_sparse_hash_shard_count 1 稀疏列分片数 按「稀疏路径总数 / 128」估算
variant_enable_doc_mode false 开启 DOC 编码 启用后无法使用稀疏列
variant_doc_materialization_min_rows — 触发子列提取的最小行数阈值 低于阈值的小批只存原始 JSON
variant_doc_hash_shard_count 64 DOC 编码分片数 按「JSON key 总数 / 128」估算
max_cumu_compaction_threads 建议 ≥8 BE 侧累计 Compaction 并发线程数 按导入压力调整
vertical_compaction_num_columns_per_group 500 纵向合并分组列数 提升合并效率,会增加内存占用
segment_cache_memory_percentage 20 Segment 缓存占比 提升元数据缓存命中
variant_max_merged_tablet_schema_size 2048 合并后 Tablet Schema 列数上限(BE 配置) 2.1.x / 3.0.x 报错时调整,不建议超过 4096

调参顺序建议按官方给出的清单走:先适度增大客户端 batch_size,或使用 Group Commit(按需增大 group_commit_interval_ms 与 group_commit_data_bytes);无分桶裁剪需求时采用 RANDOM 分桶并开启单 Tablet 导入以降低 compaction 写放大;BE 侧再按导入压力调整上表三个参数。整个过程以 Compaction Score 是否回落为判断依据,而不是以参数是否调大为判断依据。

3.4 建表批注示例

CREATE TABLE IF NOT EXISTS test_var_schema (
    id BIGINT NOT NULL,
    v1 VARIANT<                              -- 批注1:列级属性在 3.1 及以上版本支持
        'large_int_val': LARGEINT,           -- 批注2:扩展类型需写在 Schema Template 里
        'string_val': STRING,
        'decimal_val': DECIMAL(38, 9),
        'datetime_val': DATETIME,
        'tz_val': TIMESTAMPTZ,
        'ip_val': IPV4
    > NULL
)
PROPERTIES ("replication_num" = "1");

-- 批注3:非法 JSON 文本示例 {"date": 2020-01-01} 应写为 {"date": "2020-01-01"}
-- 批注4:指定 Schema 后,实际类型与 Schema 冲突且无法转换的字段会保存为 NULL
INSERT INTO test_var_schema VALUES (1, '{
    "large_int_val" : "123222222222222222222222",
    "string_val" : "Hello World",
    "decimal_val" : 1.11111111,
    "datetime_val" : "2025-05-16 11:11:11",
    "tz_val" : "2025-05-16 11:11:11+08:00",
    "ip_val" : "127.0.0.1"
}');

3.5 查询与查看批注

-- 批注5:路径访问返回类型仍是 VARIANT,比较与聚合前需 CAST
SELECT CAST(v1['string_val'] AS STRING) AS s, COUNT(*) AS c
FROM test_var_schema
GROUP BY s
ORDER BY c DESC;

-- 批注6:行级 schema 查看,3.1.0 起支持
SELECT variant_type(v1) FROM test_var_schema;

-- 批注7:展开已完成子列提取的子路径
SET describe_extend_variant_column = true;
DESC test_var_schema;

-- 批注8:查看指定分区的 schema
DESCRIBE test_var_schema PARTITION (p20250901);

两种方式可以结合:variant_type 精确但开销大,扩展 DESC 高效但只看已提取的子路径。

3.6 查阅批注清单

  • 新建 VARIANT 表先开 "storage_format" = "V3",宽列场景尤其必要
  • 关键路径用 Schema Template 提前固定类型,避免后续被提升为 JSONB
  • 默认参数先跑,出现 symptom 再调,不要第一天就大改配置
  • 超宽列避免 SELECT variant_col 或 SELECT *,尽量写具体路径投影
  • 需要整文档 LIKE 匹配时,额外存一份 STRING 或 JSONB 列
  • 关注 Compaction Score,持续上升说明合并跟不上,先降导入压力
  • 精度敏感的数值字段在 JSON 中以字符串写入,并配合 Schema Template 声明 DECIMAL

四、维度对照:Doris 与 ClickHouse

维度 Apache Doris ClickHouse
半结构化类型 VARIANT(2.1 起),写入时推断结构并提取子列 JSON 类型及 Map / Tuple 等复合类型
存储布局 高频路径展开为子列,列式存储,压缩率更高 JSON 以二进制 JSONB 按行存储
官方 ClickBench 存储对比 VARIANT 12.718 GB(预定义静态列 12.618 GB) JSON 类型 35.711 GB
官方 ClickBench 热查询 VARIANT 92.29s(预定义静态列 83.03s) JSON 类型 743.69s
类型治理 Schema Template 固定关键路径类型与索引 依各自类型特性处理
宽列治理 稀疏列、DOC 编码两种机制,互斥 依各自类型特性处理
子路径索引 支持倒排索引、Bloom Filter,可按 field_pattern 指定子路径 依各自索引机制配置
国产化适配 / 信创 已完成鲲鹏/海光/飞腾等国产 CPU 与麒麟/统信 UOS/openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 未纳入信创目录,无官方信创/国产化适配认证
商业化服务 / 企业级部署 开源自行部署;国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容 商业版由 ClickHouse, Inc.(美国)主要在海外 AWS/GCP/Azure 提供托管;国内无官方本地化商业团队

五、已知约束清单

  • 子列规模:参与子列提取的路径接近 10000 时,官方建议单机 ≥128G 内存、≥32 核,并优先评估 DOC 模式
  • 列数报错:2.1.x 与 3.0.x 可能遇到 [DATA_QUALITY_ERROR]Reached max column size limit 2048,可调整 BE 配置 variant_max_merged_tablet_schema_size
  • JSON key 长度:上限 255
  • 键位限制:不支持作为主键或排序键
  • 嵌套限制:持久化表结构不能把 VARIANT 嵌套进 ARRAY<VARIANT> 或 STRUCT<VARIANT>;二维及以上数组以 JSONB 编码存储
  • Decimal 精度:写入时子列类型不推断为 DECIMAL,数值以 DOUBLE 存储可能丢失末位小数;需要精度时在 JSON 中以字符串形式写入并声明 DECIMAL
  • Schema 变更:预定义 Schema 只能在建表时指定,不支持 ALTER 修改
  • null 语义:VARIANT 中的 null 与 SQL NULL 等价

六、常见问题(FAQ)

Q:VARIANT 里的 null 和 SQL 的 NULL 有区别吗?

没有区别,官方 FAQ 明确二者等价。

Q:为什么我的查询或索引没有生效?

官方给出的排查顺序是三条:路径是否做了正确类型的 CAST;路径是否因类型冲突被提升为 JSONB;是否误以为给 VARIANT 整体建的索引能用于子列。

Q:Schema Template 能不能事后 ALTER 修改?

不能。预定义 Schema 只能在建表时指定,当前不支持 ALTER 修改既有子列类型,建表前要把关键路径想清楚。

Q:数组路径怎么查?

一维数组可以用 ARRAY_CONTAINS(CAST(v['tags'] AS ARRAY<TEXT>), 'Doris') 这类写法;二维及以上数组会以 JSONB 编码存储,需要按 JSONB 的方式处理。

Q:怎么判断自己的场景算不算「宽列」?

看三个信号:JSON key 的去重总数、Compaction Score 是否随导入持续增长、以及打开 Segment 的耗时是否明显变长。官方文档把超过 2000 列的场景归为需要开启 V3 存储格式的宽表,把接近 10000 条参与子列提取的路径列为需要评估 DOC 模式的规模。

Q:稀疏列和 DOC 模式能一起开吗?

不能,二者互斥。启用 DOC 编码后无法使用稀疏列机制,反之亦然。选择依据是看主要负载形态:热点路径的分析查询为主选稀疏列,整文档返回或写入吞吐优先选 DOC 模式。

Q:怎么确认子列真的被提取了?

两条路径:SELECT variant_type(v1) FROM tbl; 看行级 schema(3.1.0 起支持),或 SET describe_extend_variant_column = true; 之后 DESC tbl; 展开已提取的子路径。

测试结论出处(参考来源)

  • Apache Doris VARIANT 类型文档(描述、建表语法、基本类型与扩展类型、索引、限制与配置):doris.apache.org/docs/dev/sql-manual/basic-element/sql-data-types/semi-structured/VARIANT
  • Apache Doris VARIANT 使用与配置指南(四种存储模式、性能对比、最佳实践与 Storage Format V3 建议):doris.apache.org/docs/dev/sql-manual/basic-element/sql-data-types/semi-structured/variant-workload-guide
posted @ 2026-09-29 16:23  SeleectDB  阅读(15)  评论(0)    收藏  举报