今天翻到一篇不错的技术分享,看完之后自己也琢磨了一下,把思路梳理记录下来。
摘要:随着数据不断演进,尤其在 AI Agent、LLMOps 与可观测性系统快速发展的背景下,宽 JSON 的字段膨胀带来巨大的性能挑战。Apache Doris 4.1 通过 Doc Mode 与 Segment V3 达成了高效的存储与查询优化,使系统在写入吞吐、查询性能与资源开销之间保持良好平衡,其综合性价比优于 Clickhouse、PostgreSQL 等业界典型方案。
随着业务持续演进(车型上线、埋点变更、模型升级),系统不断引入新的字段,导致字段集合快速膨胀。当字段并集达到万级、单行数据高度稀疏且查询需求频繁变化时,传统的预定义 Schema 已难以满足需求。典型特征包括:字段规模从数百到上万不等,演进速度快、分布分散、写入吞吐量高,同时查询往往只关注少数字段。
典型应用场景如下:
- 车联网:车型、硬件与 OTA 更新叠加,字段随设备与版本频繁变化,规模可达万级,且车型间差异显著。
- 可观测性:微服务与 SDK 持续迭代,日志与 Trace 维度不断扩展至数百至上千字段。
- 行为分析:业务扩张带来属性变宽,成熟阶段字段规模约 1k~5k。
- AI 应用:Prompt 与模型快速迭代,Agent Trace、Tool 调用、RAG 检索结果及评估数据持续引入新 Key,字段结构随模型与干活流变化不断演进。
1. 核心性能瓶颈
随着吞吐量持续攀升,当 JSON path 扩展到万级时,系统主要面临两个核心瓶颈:
- 元数据膨胀:随着列/字段规模的增长,元数据(如 Footer 和 metadata)的体积会随列数线性增长,导致系统存储压力骤增。这不仅仅是个别现象,而是列式存储普遍面临的挑战。
- 过早子列物化:为了提升查询性能,一些系统(如早期的 Doris Variant 默认达成和 Elasticsearch 等)会在写入时将每个 JSON path 立即物化为独立列。这种做法会带来写入和 Compaction 的双重压力。
2. 常见的解决方案
在应对万级以上宽 JSON 的挑战时,不同系统给出了各具侧重的技术路径,但本质上都在 灵活性、写入成本与查询性能 三者之间做权衡。
参考链接
Making complex JSON 58x faster, use 3,300x less memory, in ClickHouseMapping explosion | Elastic DocsThe Apache Iceberg™ Variant Type: Flexible Semistructured Data, Reimagined深入搞懂 Doris Variant:如何让 JSON 查询性能追平列存,还能承载万列索引字段?|Deep Diveparquet-format/VariantShredding.md at master · apache/parquet-format
我们选取 ClickHouse 的 Advanced Serialization 与 PostgreSQL 的 JSONB 两种具有代表性的方案进一步分析。
2.3 ClickHouse:控制列数但编码效率受限
ClickHouse v25.8 引入了 Advanced Shared Data 序列化格式(参考 Making complex JSON 58x faster),用于缓解 JSON path 过多时导入性能急剧下降的麻烦。这一方案通过固定的 bucket 数控制文件总数,避免了列数多时完全不可用的问题。
这一方案在可用性上做了明显改善,但也带来了新的代价:受 ClickHouse 类 PAX(Partition Attributes Across)存储布局达成机制的影响,数据按列或属性被分散组织,查询时往往要在多个存储位置之间反复定位和跳转,从而导致随机读放大;同时,为了兼顾写入与合并流程,系统还要额外保留一份原始数据副本,进一步推高存储成本。整体来看,问题虽然得到一定缓解,但在大规模场景下,查询性能和资源开销仍然不够理想。
2.4 PostgreSQL:文档友好但分析场景受限
PostgreSQL 的 JSONB 类型将 JSON 解析为二进制格式存储,支持 GIN 索引做键值检索。在点查和文档回读(SELECT*)场景下,JSONB 表现出色——原始文档不要从大量子列中重新拼接。
但其本质仍是行式存储,缺乏列式优化。在分析场景(如按字段过滤、聚合)下,即使只查询单个 key,也需逐行解析 JSON,难以利用列式带来的压缩与计算优势,随着数据规模增长,查询延迟会显著上升。
3. Doris 4.1:延迟物化 + 按需加载
针对上述问题,Apache Doris 在最新发布的 4.1 版本中引入了 Doc Mode 与 Segment V3 两项关键能力,分别从写入路径与元数据管理两个层面进行优化:
- Doc Mode(延迟子列物化):在写入阶段以文档形态高效落盘,仅在 Compaction 阶段按需物化高频 JSON path,从而显著降低写入放大与系统压力,提升整体吞吐能力。
- Segment V3(按需加载元数据):将原本集中在 Footer 中的列级元数据拆分为独立存储结构,查询时仅加载相关列的元数据,在万列规模下有效降低内存占用与 I/O 开销。
那么,相较于业界典型方案 ClickHouse 和 PostgreSQL,Doris 是否更具优势?
从机制上看:
- 对比 ClickHouse:Doris 在子列物化后采用纯列式连续存储,避免了类似 PAX 布局带来的随机读放大问题;同时在默认策略下无需保留冗余副本,存储开销更可控。
- 对比 PostgreSQL(JSONB):一旦 JSON path 被物化,Doris 即可充分发挥列式存储在压缩与向量化计算方面的优势;而 JSONB 始终受限于行式存储模型,其 I/O 模式在分析场景下存在天然瓶颈。
- Doris vs ClickHouse:在宽 JSON 的聚合与过滤场景中,Doris 查询延迟稳定在百毫秒级;而 ClickHouse 受 Advanced 编码带来的随机读影响,查询延迟上升至数秒级。同时,在默认配置下,Doris 的存储空间约为 ClickHouse 的 60%。
- Doris vs PostgreSQL:PostgreSQL 在文档整行回读(如 Q3)场景中表现突出;但在聚合与过滤场景(Q1 / Q2)下,其查询延迟相较 Doris 存在数量级差距,可达数百倍。
4. Doc Mode:按需延迟子列物化
在 Doris 4.1 中,Doc Mode 将写入、合并与查询解耦为三个阶段处理,从而在保证写入吞吐的同时,查询性能的渐进优化:
- 写入阶段,仅以文档形态存储 JSON
- 合并阶段(Compaction):在合适时机按需将高频 path 物化为子列
- 查询阶段:根据字段的物化状态,自动选择最优执行路径
4.1 写入阶段:优先把 JSON keys 编码落盘为 Hash Sharded
为优先保障写入吞吐,系统将 JSON 数据编码为 Sharded Map,以哈希分片的方法进行结构化落盘。这一设计既支持 SELECT * 的高效整行返回,也为未物化字段给出统一的兜底存储。
原始 JSON 被拆分到多个独立的列式 Map<Binary, Binary> 分片中,并通过variant_doc_hash_shard_count参数控制分片数量,使数据均匀分布。
在查询 fallback 场景下,仅需命中并扫描对应的单一分片,避免全量数据扫描带来的性能开销。由此,写入复杂度从“随未知 Path 数量增长”转变为“面向固定分片结构”,显著提升系统稳定性与可扩展性。
4.2 合并阶段:推迟物化
参数 variant_doc_materialization_min_rows用于定义 path 的物化时机:当数据批次较小或尚未沉淀时,仅写入 Sharded Map;只有在触发 Compaction 且行数达到阈值后,才会将高频 path 抽取为独立子列。相比即时列化,这种延迟决策机制显著提升了突发写入场景下的系统稳定性。
图 3:写入先落 Doc Map,Compaction 阶段再把常用字段抽成列。
4.3 查询阶段:三档读路径的自动切换
对于上层查询引擎而言,Doc Mode 会根据字段的物化状态,将请求自动路由到最合适的读取路径,实现性能与灵活性的动态平衡:
DOC Materialized:热点字段已被抽取为子列,直接走纯列式读取路径,查询效率最高,同时可充分利用索引下推等优化能力。DOC Map:用于SELECT *或整文档读取场景,直接返回原始 Doc,无需进行子列拼接,整体开销极低。DOC Map (Sharded):针对未物化且无法整文档返回的冷字段查询,请求会被定向路由到对应的哈希分片,仅扫描相关 shard,大幅降低无效数据扫描。
图 4 说明:
- 已物化字段走列式路径(~76 ms),充分利用列存优势;
- 未物化但分片的字段走单 shard 扫描(~148 ms);
- 其余场景则回退至 Doc Map 全量扫描(~2,533 ms)。
4.4 JSON key 演化限制,提升系统稳定性
在 Agent Trace 或缺乏规范约束的上游数据场景中,JSON key 往往持续演化(例如从 score_* 演变为 tool_result.*),缺乏稳定边界。此时,可以适当提高 variant_doc_materialization_min_rows 阈值,使大量短生命周期或低频字段停留在 Doc Map 形态,从而实现更稳健的系统行为:
- 稳定写入路径:避免后台频繁触发大规模字段物化与字典编码,降低写放大与 Compaction 压力
- 高效整文档读取:
SELECT *可直接返回原始记录,无需子列拼装,开销更低 - 可控的冷字段访问:低频 path 仍可通过分片化 Doc Map(Sharded)定向访问,虽略低于列式性能,但有效避免列数无限膨胀带来的系统性负担
4.5 适用场景
在以下三类典型业务中,Doc Mode 往往可以显著提升整体效果:
1. 高稀疏日志/事件数据:字段并集规模巨大,但单条记录仅命中少量 key,且结构高度离散
2. 写入压力与字段膨胀并存:由于高吞吐写入与字段快速增长,已出现明显的 Compaction 积压或写放大问题
3. 混合访问模式(分析 + 回读):既需要对字段进行检索分析,又需要不定期进行整文档回读(SELECT *)
5. Segment V3:按需加载的元数据格式
在 Doris 4.1 之前,系统采用 Segment V2 存储格式,将所有列的元数据集中存放在文件尾部(Footer)。这一设计在大规模顺序扫描场景中表现高效,但在随机读取或小范围查询时,每次都需要加载完整元数据,带来额外的 I/O 与解析开销,成为性能瓶颈。
为此,Doris 4.1 引入 Segment V3,借鉴了 Lance 以及 Vortex 等新型文件存储格式的做法,将元数据从 footer 中分离,按需加载,解决万列场景下最容易碰到的元数据膨胀、文件打开慢和随机读开销问题,在初始读取阶段的性能提升尤为显著。适用于超宽表、大量 VARIANT 子列、对象存储冷启动敏感、随机读较多的 AI 和车联网半结构化数据场景。例如 AI Observability、Prompt Debug 与在线推理分析等场景,通常只会访问少量动态 path,但需要快速完成随机查询。
开启 Segment V3 后,Doris Doc 的冷热查询性能更加均衡,避免了明显的性能分层。
以一张包含 7,000 列、10,000 个 Segment 的超宽表为例。在 Segment 打开阶段,V3 相比 V2 实现了显著提升:
- 打开速度提升最高达 16 倍
- 内存占用降低最高达 60 倍
6. 综合性能验证
为了评估各方案在宽 JSON 场景下的实际表现,我们设计了一组贴近真实业务的基准测试。测试环境与约束如下:
数据特征:
- JSON key 并集规模约 10K(无法提前定义 Schema)
- 每行随机写入 100 个 key,value 为随机数,模拟高稀疏宽表
- 总数据量 1 亿行(约 160GB),拆分为 1000 个文件,模拟高频导入场景
产品及配置
访问模式:高并发写入 + 随机字段查询(避免针对特定字段优化)
6.1 存储与导入性能
存储空间:如上图可知,Doris 的 Variant Default 最佳,得益于全列式存储、无需冗余数据。
导入性能: PostgreSQL(JSONB)最高,其次为 Variant Doc,两者明显优于 ClickHouse、Elasticsearch 以及 Variant Default
6.2 查询延迟 (冷查 / 热查)
由上可知,不同系统在单项能力上各有侧重:PostgreSQL 在文档型存储与回读场景下(Q3)表现较好,但复杂分析能力受限;ClickHouse 在字段规模可控时具备优秀的列式分析能力,但在超宽 JSON 场景下容易受到路径数量、元数据和 Shared Data 机制影响。
相比之下,Doris 基于延迟物化(Doc Mode)+ 按需元数据加载(Segment V3),在宽 JSON 场景下同时兼顾写入吞吐、存储控制、复杂分析和整文档读取,在性能与资源开销之间取得了更优平衡,展现出更强的综合能力。
6.3 综合结论
综合存储、导入和查询结果可以看出,Doris Variant Doc Mode + Segment V3 的价值在于,它不是单纯选择文档存储或列式存储中的某一种,而是通过文档写入、延迟物化、按需元数据加载和列式查询执行的组合,在宽 JSON 场景下形成更适合分析型业务的折中方案,是相比传统 JSONB、搜索引擎方案以及纯列式 JSON 方案更好的一种实现路径。
7. 快速上手验证
如果各位正被 JSON key 持续膨胀所困扰,可以通过以下最小可行配置快速进行验证:
CREATE TABLE IF NOT EXISTS sensor_data (
ts DATETIME NOT NULL,
device_id VARCHAR(64) NOT NULL,
model VARCHAR(128),
data VARIANT< -- Schema Template,按需设置列属性
'bat_temp' : DOUBLE,
properties(
'variant_enable_doc_mode' = 'true'
)
>,
INDEX idx_data(data) USING INVERTED PROPERTIES("field_pattern" = "status")
)
DUPLICATE KEY(`ts`, `device_id`)
DISTRIBUTED BY HASH(`device_id`) BUCKETS 16
PROPERTIES (
"replication_num" = "1",
"storage_format" = "V3"
);
注:在后续版本中,storage_format = "V3" 预计将成为宽表场景的默认选项。
查询语法同样简洁直观:
SELECT
ts,
CAST(data['bat_temp'] AS DOUBLE) AS bat_temp
FROM sensor_data
WHERE bat_temp > 60
ORDER BY ts DESC
LIMIT 100;
8. 收尾语
随着 AI 应用逐渐进入 Agent 化与实时化阶段,动态字段持续增长将成为长期趋势,这也对半结构化数据系统的灵活性与扩展性提出了更高要求。通过引入 Doc Mode(延迟物化) 与 Segment V3(按需加载),Doris 在保持高写入吞吐的同时,有效缓解了元数据膨胀与查询性能退化问题,尤其在高并发、高稀疏、高演化的宽 JSON 场景中展现了明显的优势。无论是车联网、可观测性、行为分析,还是 AI 应用等领域,Doris 都可以有效应对数据字段膨胀与查询性能下降的挑战,给出了一个稳定、可扩展的解决方案。
参考资料
- Apache Doris 4.x VARIANT 使用与配置指南
- 深入搞懂 Doris Variant:如何让 JSON 查询性能追平列存,还能承载万列索引字段?|Deep Dive
- ClickHouse: Making complex JSON 58x faster
- Apache Iceberg Issue #5219: The metadata file is too large
- Apache Iceberg Issue #9734: Improve read times by storing schemas in external files
- Elasticsearch: Mapping Explosion
- Snowflake: Apache Iceberg V3 Variant Type
- parquet-format/VariantShredding.md at master · apache/parquet-format
这篇笔记就先到这里,后面用到新的思路或者发现有问题再补充。
评论 (0)
暂无评论