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

摘要 :埋点事件的字段常随版本增减,逐条声明列的建表方式会持续产生 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 Elasticsearch JSONBench 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 等价
BigInt 64 位整数
Double 浮点数,数值默认推断为该类型
String (Text) 字符串;日期时间等非标准 JSON 类型默认存为字符串
Jsonb 类型冲突后提升为该类型
Variant 嵌套对象
Array 仅支持一维

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

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

源类型 当前类型 最终类型
TinyInt BigInt BigInt
TinyInt Double Double
TinyInt String JSONB
TinyInt Array JSONB
BigInt Double JSONB
Double String JSONB
Array Array Array
Array Array Array

3.2 建表写法速查

sql 复制代码
-- 基础写法:静态列 + 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"
);
sql 复制代码
-- 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 查询写法速查

sql 复制代码
-- 子路径取值: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 索引写法速查

索引类型 写法 适用过滤
BloomFilter PROPERTIES("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 默认 false Schema Template 声明的路径是否计入上述阈值
variant_sparse_hash_shard_count 默认 1 稀疏列分片数,分散稀疏子列读取开销
variant_enable_doc_mode 默认 false 原始 JSON 作为存储字段保存,加速整条 JSON 返回,与稀疏列互斥
variant_max_merged_tablet_schema_size 2.1.x / 3.0.x 默认 2048,不建议超过 4096 tablet 合并后的 schema 列数上限
describe_extend_variant_column SET 为 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:

css 复制代码
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:

bash 复制代码
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 Doris Elasticsearch
类型声明 props VARIANT,写入时推断子列结构 dynamic mapping 写入时自动新增字段
类型固定 Schema Template 声明 Decimal / Datetime / IPV4 等扩展类型 mapping 中字段类型确定后不可修改,调整需重建索引
字段数约束 variant_max_subcolumns_count 默认 2048,超出后低频路径进入共享结构 index.mapping.total_fields.limit 默认 1000,超限后写入被拒绝
类型冲突处理 可兼容时提升为公共类型,无法提升时该路径回退 JSONB 字段类型不可变更,需重建索引
检索与聚合 同一条 SQL 内完成子路径过滤与聚合,支持标准 SQL DSL(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 倍。两者互斥,不能同时开启。

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

相关推荐
l1t1 小时前
几种数据库软件count(distinct)和count()from group by子查询性能测试
开发语言·数据库
计算机编程指导师1 小时前
计算机毕设选题推荐:基于Spark+Django的黄金价格历史数据分析与可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·python·计算机·spark·django·课程设计·黄金价格
夜雪一千1 小时前
如何在MySQL中实现字段级加密
android·数据库·mysql
CJi0NG1 小时前
【自用】MySQL-存储引擎
数据库·mysql
weixin_460443561 小时前
在线考试人脸识别如何合规?本地活体检测、人脸模板加密与替代认证的技术架构设计
大数据·数据库·煤矿培训考试系统·煤矿在线考试系统·煤矿安全培训·煤矿考试系统
计算机编程指导师1 小时前
计算机毕设怎么做?基于Spark+Django的股票数据分析与可视化系统实战指导 源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·python·spark·django·毕业设计·股票·课程设计
deepseek232 小时前
SQLite WAL 开了仍报 database is locked?Python 后台任务的事务与幂等修复
数据库·python·sqlite·并发编程·wal
SelectDB2 小时前
冷热分层速查手册:参数、命令、生效判据与常见失效原因一页看完
大数据·数据库·数据分析