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 为以上任意类型,仅支持一维)
批注三条:
- 预定义 Schema 只能在建表时指定,当前不支持通过 ALTER 修改。
- 支持前缀通配符,
'message*' : STRING表示前缀匹配 message 的子列统一为 STRING;同一路径命中多个 pattern 时,取定义顺序中第一个匹配的类型。 - 列名本身含
*且要按名称精确匹配时,写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

浙公网安备 33010602011771号