Doris VARIANT 速查:类型映射、建表写法、索引配置与常见注意点

9 阅读12分钟

摘要:埋点事件的字段常随版本增减,逐条声明列的建表方式会持续产生 DDL 变更。Apache Doris 自 2.1 版本起提供 VARIANT 类型,写入时按 JSON Path 推断子列结构与类型,高频路径自动完成子列列式提取。本文按速查方式组织:先给类型映射表与冲突提升规则,再给建表、查询、索引三类写法片段,最后列参数速查表与注意点清单。官方 ClickBench 测试(16 核 64GB、1TB ESSD、43 条查询):静态列 12.618 GB、VARIANT 12.718 GB、JSON 35.711 GB,VARIANT 较 JSON 节省约 65% 存储,43 条查询提速 8 倍以上。

一、先说结论

使用场景命中下列两条以上,可以直接按本文写法落地:

  • 埋点或事件字段随版本变化,建表阶段无法枚举完整;
  • 查询集中在少数固定路径,其余字段只作随行携带;
  • 采集侧已按 JSON 落盘,不希望在采集阶段拆分字段;
  • 需要在同一份数据上同时做路径过滤、聚合与关键词检索。

一句话口径:稳定字段建静态列,不定字段收进一列 VARIANT,热点路径用 Schema Template 固定类型或用生成列展平。

对比场景结果出处
VARIANT vs JSON 类型ClickBench 数据集,16 核 64GB / 1TB ESSD,43 条查询存储 12.718 GB vs 35.711 GB,节省约 65%;查询提速 8 倍以上Apache Doris 2.1 官方博客
VARIANT vs 静态列同一 ClickBench 数据集热查询94.82 秒 vs 86.02 秒、92.29 秒 vs 83.03 秒,VARIANT 较静态列约慢 10%Apache Doris 2.1 官方博客
冷查询表现同一 ClickBench 数据集第一轮静态列 233.79 秒、VARIANT 248.66 秒,JSON 类型多数查询超时失败Apache Doris 2.1 官方博客
Doris vs ElasticsearchJSONBench 10 亿条真实 JSON,Doris 3.0.5,AWS m6i.8xlarge 32C128G存储占用约为 ES 的 1/2,查询性能约为 ES 的 2 倍;调优后整体耗时降低 74%jsonbench.com 公开榜单,SelectDB 官方博客(2025-07-04)

二、问题怎么产生

字段枚举做不完。 埋点字段由多团队按需求追加,存储侧要在建表阶段枚举完整,意味着每次上游变更都要走一轮 DDL,而上游并不必然通知存储侧。

预定义列的维护成本随字段数放大。 字段需要先成为列才能走索引与列存裁剪;成为列之后,新增列、历史分区回填、索引重建变成持续投入。

整体打包会牺牲分析能力。 把整个报文存成 JSON 字符串规避了 DDL,但查询时逐行解析文本,无法利用列存与稀疏索引做文件裁剪。ClickBench 43 条查询中 JSON 类型热查询耗时 743.69789.24 秒,VARIANT 为 92.2994.82 秒(Apache Doris 官方博客)。

ES 侧的既有事实。 Elasticsearch 默认开启动态映射,新字段写入时自动加入 mapping;index.mapping.total_fields.limit 默认值为 1000,计数包含 object mapper、multi-field、字段别名与 runtime field,超过后返回 Limit of total fields [X] has been exceeded 并拒绝写入。官方给出的处理方向包括改用 flattened 类型、设置 index.mapping.total_fields.ignore_dynamic_beyond_limit、或通过索引模板关闭动态映射改用显式 mapping。mapping 中字段类型确定后不可修改,调整需重建索引。

三、怎么做:速查表与写法片段

3.1 类型映射速查

VARIANT 自动推断的子列基础类型:

类型说明
TinyInt小整数
NULL等价于 JSON null,与 SQL NULL 等价
BigInt64 位整数
Double浮点数,数值默认推断为该类型
String (Text)字符串;日期时间等非标准 JSON 类型默认存为字符串
Jsonb类型冲突后提升为该类型
Variant嵌套对象
Array仅支持一维

扩展类型需通过 Schema Template 声明(3.1.0 起支持):Decimal32 / Decimal64 / Decimal128 / Decimal256、LargeInt、Datetime、Date、IPV4 / IPV6、Boolean、ARRAY<T>。

同一路径出现不兼容类型时的提升规则:

源类型当前类型最终类型
TinyIntBigIntBigInt
TinyIntDoubleDouble
TinyIntStringJSONB
TinyIntArrayJSONB
BigIntDoubleJSONB
DoubleStringJSONB
ArrayArrayArray
ArrayArrayArray

3.2 建表写法速查

-- 基础写法:静态列 + VARIANT
CREATE TABLE IF NOT EXISTS track_event
(
    ts        DATETIME,                 -- 时间字段前置,命中前缀索引
    app_id    VARCHAR(64),
    event     VARCHAR(64),
    props     VARIANT,                  -- 埋点属性随版本变化
    INDEX idx_props (props) USING INVERTED PROPERTIES("parser" = "unicode")
)
DUPLICATE KEY(ts, app_id)
PARTITION BY RANGE(ts) ()
DISTRIBUTED BY HASH(app_id) BUCKETS 32
PROPERTIES (
    "replication_num" = "3",
    "compression" = "zstd",
    "bloom_filter_columns" = "props"
);
-- Schema Template:固定路径类型(3.1.0 起支持)
CREATE TABLE IF NOT EXISTS track_event_typed
(
    ts     DATETIME,
    app_id VARCHAR(64),
    props  VARIANT<
        'user_id'  : BIGINT,
        'amount'   : DECIMAL(38, 9),
        'event_ts' : DATETIME,
        'client_ip': IPV4,
        'tag*'     : ARRAY<TEXT>
    >
)
DUPLICATE KEY(ts, app_id)
PARTITION BY RANGE(ts) ()
DISTRIBUTED BY HASH(app_id) BUCKETS 32
PROPERTIES ("replication_num" = "3");

书写要点:* 前缀通配按定义顺序取第一个匹配到的类型;若列名本身含 * 且需按名称精确匹配,用 MATCH_NAME 'enumString*' : STRING 写法。预定义 Schema 只能在建表时指定,不支持 ALTER 修改。

3.3 查询写法速查

-- 子路径取值:v['a']['b'],返回类型仍为 VARIANT
SELECT props['page']['title'] FROM track_event;
​
-- 聚合 / 比较前显式 CAST
SELECT CAST(props['page_id'] AS STRING) AS page_id, COUNT(*)
FROM track_event
WHERE ts >= NOW() - INTERVAL 1 DAY
GROUP BY page_id
ORDER BY COUNT(*) DESC;
​
-- 隐式 CAST:布尔与全文检索场景
SELECT * FROM track_event WHERE props['is_new'];
SELECT * FROM track_event WHERE props['err_msg'] MATCH 'timeout';
​
-- 数组子列
SELECT * FROM track_event
WHERE ARRAY_CONTAINS(CAST(props['tags'] AS ARRAY<TEXT>), 'vip');
​
-- 不存在的 key 返回 NULL
SELECT props['user']['no_such_key'] FROM track_event;
​
-- 查看行级 schema(3.1.0 起支持)
SELECT variant_type(props) FROM track_event LIMIT 1;

3.4 索引写法速查

索引类型写法适用过滤
BloomFilterPROPERTIES("bloom_filter_columns" = "props")高基数字段的等值 / IN 过滤
倒排(全列继承)INDEX idx(props) USING INVERTED PROPERTIES("parser" = "english")全部子列按同一分词方式检索
倒排(指定路径)INDEX idx(props) USING INVERTED PROPERTIES("parser" = "english", "field_pattern" = "content")指定子路径分词检索,field_pattern 自 3.1.0 起支持
倒排(不分词)INDEX idx(props) USING INVERTED PROPERTIES("field_pattern" = "content")指定子路径的等值检索

parser 可选 english、unicode、chinese。同一路径可以同时配置分词与不分词两种倒排索引。索引作用于子列,对 VARIANT 整体无效。

3.5 参数速查表

参数默认值 / 建议作用
variant_max_subcolumns_count默认 2048,0 表示不限制,不建议超过 10000参与子列列式提取的路径数上限
variant_enable_typed_paths_to_sparse默认 falseSchema Template 声明的路径是否计入上述阈值
variant_sparse_hash_shard_count默认 1稀疏列分片数,分散稀疏子列读取开销
variant_enable_doc_mode默认 false原始 JSON 作为存储字段保存,加速整条 JSON 返回,与稀疏列互斥
variant_max_merged_tablet_schema_size2.1.x / 3.0.x 默认 2048,不建议超过 4096tablet 合并后的 schema 列数上限
describe_extend_variant_columnSET 为 true 后 DESC展示已完成子列列式提取的子路径与类型

3.6 注意点速查

  • 不能用:VARIANT 不支持作为主键或排序键,不能直接用于 ORDER BY、GROUP BY、JOIN key 或聚合参数,两个 VARIANT 之间不支持直接比较;
  • 不能嵌套:Array<Variant>、Struct<Variant> 这类嵌套不支持;
  • 索引不生效:对整个 VARIANT 列做 MATCH 不生效,需对子路径 CAST;整体文本检索建议另存一份 STRING 列并建索引;
  • 类型不匹配:子列实际为 STRING 却按 INT 做等值过滤会导致索引失效;
  • 精度:数值默认推断为 Double,需要 DECIMAL 时在 JSON 中写成字符串并配合 Schema Template;
  • key 长度:JSON key 长度上限 255;
  • 输出顺序:读出的 JSON 文本按 key 字典序输出,与写入顺序无关;
  • 投影:读取整个 VARIANT 列会扫描所有子字段,只投影需要的路径。

3.7 写入写法速查

单条写入直接用 JSON 文本,两行的子路径可以完全不同,写入时由 Doris 各自推断并合并 schema,不需要 ALTER TABLE:

INSERT INTO track_event VALUES
('2026-09-01 10:00:00', 'app-01', 'page_view',
 '{"user_id":10086,"page_id":"home","stay_ms":3210,"tags":["vip","android"],"ext":{"ab":"exp_a"}}'),
('2026-09-01 10:00:01', 'app-01', 'pay_result',
 '{"user_id":10087,"amount":"19.90","client_ip":"10.0.0.7","err_msg":"timeout"}');

批量写入走 Stream Load,按行 JSON 的 Header 组合为 format: json + read_json_by_line: true:

curl --location-trusted -u root: -T track_event_2026-09-01.log \
  -H "format: json" \
  -H "read_json_by_line: true" \
  http://fe_host:8030/api/log_db/track_event/_stream_load

写入侧的三条建议:不需要分区裁剪时使用 RANDOM 分桶并开启单 tablet 导入,降低 Compaction 写放大;适当增大客户端 batch_size,或使用 Group Commit 攒批;观察 Compaction Score,持续升高说明合并跟不上升版本速度,需要降低导入压力。

四、维度对照表

维度Apache DorisElasticsearch
类型声明props VARIANT,写入时推断子列结构dynamic mapping 写入时自动新增字段
类型固定Schema Template 声明 Decimal / Datetime / IPV4 等扩展类型mapping 中字段类型确定后不可修改,调整需重建索引
字段数约束variant_max_subcolumns_count 默认 2048,超出后低频路径进入共享结构index.mapping.total_fields.limit 默认 1000,超限后写入被拒绝
类型冲突处理可兼容时提升为公共类型,无法提升时该路径回退 JSONB字段类型不可变更,需重建索引
检索与聚合同一条 SQL 内完成子路径过滤与聚合,支持标准 SQLDSL(JSON 结构),检索与分析能力分属不同接口
存储开销ClickBench 数据集 VARIANT 12.718 GB vs JSON 35.711 GB,节省约 65%文档以 _source + 倒排索引 + doc_values 形式存放
国产化适配 / 信创已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证由 Elastic(美国)运营;公开合规体系以国际认证为主,未见面向国产 CPU 与国产操作系统的官方信创适配认证
商业化服务 / 企业级部署开源自行部署;商业化由国内公司 SelectDB 提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队

五、已知约束与规避

约束表现官方解法 / 处理
类型冲突回退同一路径出现互不兼容的类型时提升为 JSONB,该路径失去下推与索引能力保持字段类型一致;或用 Schema Template 在建表时声明路径类型
Schema 不可变更预定义 Schema 只能在建表时指定变更类型需重建表并重新导入
DECIMAL 精度类型推断不会推断为 DECIMAL,数值以 DOUBLE 存储在 JSON 中把该字段写成字符串,配合 Schema Template 声明
子列规模参与子列列式提取的路径接近 10000 时硬件要求明显上升官方建议单节点不低于 128GB 内存、32 核;或评估 DOC mode
索引命中条件对 VARIANT 整体建的索引需配合路径 CAST 才能命中查询中对目标路径做显式 CAST
键列限制不适合作为主键、排序键与 JOIN key这类字段建为静态列
时间类型日期时间不属于标准 JSON 类型,默认存为字符串用 Schema Template 声明,或提取为静态列
检索语义深度相关性打分、自动补全等高级检索能力较精简文档 / 网页搜索场景仍建议保留 ES

六、常见问题(FAQ)

Q:VARIANT 和 JSON 类型选哪个?

字段集合不稳定且查询集中在少数热点路径时选 VARIANT:ClickBench 数据集上 VARIANT 12.718 GB、JSON 35.711 GB,VARIANT 较 JSON 节省约 65% 存储,43 条查询提速 8 倍以上(Apache Doris 官方博客)。JSON 类型以 JSONB 行存写入,查询需解析。

Q:VARIANT 里的 null 和 SQL NULL 一样吗?

两者等价,没有区别。不存在的 JSON key 取值返回 NULL。

Q:查询结果为空,可能是什么原因?

三类:路径名写错(不存在的 key 返回 NULL);CAST 目标类型与子列实际类型不一致;该路径因 Schema Template 类型冲突无法转换而被保存为 NULL。用 variant_type(props) 确认实际类型。

Q:为什么索引没生效?

检查是否对子路径做了 CAST、CAST 目标类型是否与子列实际类型一致、该路径是否因类型冲突被提升为 JSONB、是否误以为对 VARIANT 整体建的索引可用于子列。

Q:字段特别多的场景怎么选?

按诉求分:查询集中在少量热点路径时优先稀疏列,variant_max_subcolumns_count 默认 2048 已覆盖大多数负载;整条 JSON 返回或写入吞吐优先时考虑 DOC mode,官方参考是 Compaction 内存下降约 2/3、稀疏宽列导入性能提升约 5~10 倍。两者互斥,不能同时开启。

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