第 15 关 · ★★★★

半结构化与超宽表

VARIANT、稀疏列、Schema Template 与 V3 存储格式:超宽表的现代打法。

已点亮 · 最佳 分
半结构化与超宽表 第 1 页

Day 14|Doris 宽表与半结构化数据:VARIANT、稀疏列、Schema Template 与 Wide Table Storage V3

Day 14 学习总览
Day 14 学习总览

前面的十三天,我们已经把 Doris 的基本坐标系、架构、表模型、分区分桶、导入、索引、物化视图和高并发点查逐层搭了起来。到这里,很多人会遇到一类很现实的问题:业务字段开始变得越来越难以提前定义。

日志平台每天会接入新的应用;埋点平台每个版本都可能新增事件属性;IoT 设备不同型号拥有不同传感器;用户画像会不断增加新的标签;广告、风控、AI 应用还会把大量上下文以 JSON 形式送进数据平台。刚开始时,一张表可能只有几十个字段,半年后可能已经膨胀到几百列,甚至出现数千、数万个可能的字段路径。

如果每增加一个字段都执行一次 ALTER TABLE,Schema 变化会逐渐变成系统维护负担。如果把所有内容都塞进字符串 JSON,Schema 维护压力降低了,但查询时又需要反复解析整段 JSON。字段越来越多以后,存储元数据、索引、Compaction、Segment 打开、冷数据随机读等成本也会持续增长。

Doris 近几个版本针对这类问题形成了一套完整能力链:VARIANT 负责吸收动态 JSON,Subcolumnization 把热点路径转成列式子列,Sparse Columns 控制长尾字段数量,Schema Template 固定关键业务路径的类型和索引,Storage Format V3 进一步降低超宽表的 Segment 元数据开销。

这一天的目标,是把这几项能力放到同一套建模方法中理解。学完以后,你应该能够回答五个问题:

  1. 哪些动态字段应该进入 VARIANT,哪些字段仍然适合普通列;
  2. JSON Path 数量持续增长时,何时需要启用 Sparse Columns;
  3. 哪些业务字段值得进入 Schema Template;
  4. Doris 4.1 的 Storage Format V3 解决了宽表的哪类底层问题;
  5. 面对日志、遥测、用户画像、业务宽表时,怎样设计一张长期可治理的表。

一、先理解“宽表问题”到底从哪里来

很多团队把“宽表”简单理解成“列很多的表”。这个定义太粗。真正需要警惕的情况,是字段数量、字段稀疏度、字段变化速度和查询模式同时发生变化

举一个埋点平台的例子。最开始只有几种事件:

{
  "event": "login",
  "user_id": 1001,
  "ts": 1723000000,
  "os": "Android"
}

后来商品详情增加实验分组:

{
  "event": "product_view",
  "user_id": 1001,
  "ts": 1723000100,
  "product_id": 888,
  "exp_group": "B",
  "recommend_model": "v7"
}

再过几个月,直播、广告、会员、搜索、风控、推荐、活动页都开始往埋点里增加自己的字段。全局看下来,可能出现几千个 JSON Path;但任何一条事件只携带其中十几个或几十个字段。此时表具有两个非常典型的特征:

  • Schema 很宽:所有可能出现的字段非常多;
  • 数据很稀疏:单行真正有值的字段很少。

这类结构用传统静态列存储,会出现大量 NULL;用纯 JSON 字符串存储,又失去了列式数据库最重要的一部分能力。Doris 的 VARIANT 设计正是为了处理这一类“结构变化快、查询仍然需要列式性能”的数据。

你提供的原培训材料已经把这一问题放在 Schema Change 章节中讨论:面对完全不可预测的 JSON 或上千字段的宽表,推荐用 Variant 吸收长尾字段;游戏日志热更新案例也采用“固定公共列 + Variant 扩展列”的方式承接新活动字段。这个方向是正确的,Day 14 会把它扩展成一套更完整的工程方法。

半结构化能力地图
半结构化能力地图


二、VARIANT:动态 JSON 进入列式数据库的入口

Apache Doris 4.x 官方对 VARIANT 的定义非常清晰:它面向结构会随时间变化的 JSON。数据写入时,Doris 会解析文档中的 JSON Path,为每条路径推断类型,并把适合独立存储的路径写成列式子列。

这一步叫 Subcolumnization

VARIANT Subcolumnization
VARIANT Subcolumnization

假设我们创建一张事件表:

CREATE TABLE event_log (
    event_time DATETIME(3),
    user_id BIGINT,
    event_name VARCHAR(64),
    props VARIANT
)
DUPLICATE KEY(event_time, user_id, event_name)
PARTITION BY RANGE(event_time) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 16;

写入:

{
  "device": {"os": "Android", "brand": "Xiaomi"},
  "page": "/goods/888",
  "exp": {"group": "A"},
  "metrics": {"clicks": 3}
}

查询时仍然可以用 JSON Path:

SELECT
    props['device']['os'] AS os,
    props['exp']['group'] AS exp_group,
    CAST(props['metrics']['clicks'] AS INT) AS clicks
FROM event_log
WHERE event_time >= '2026-08-01'
  AND props['device']['os'] = 'Android';

从 SQL 视角看,我们仍在访问 JSON。存储层看到的情况已经发生变化:device.osexp.groupmetrics.clicks 等路径可以形成独立子列,每个子列拥有自己的编码、压缩、ZoneMap,以及可选索引。查询 props['device']['os'] 时,系统没有必要重复解析整段原始 JSON。

这就是 VARIANT 的关键价值:Schema 可以继续变化,热点查询路径仍然进入列式执行链路。

2.1 类型推断和类型提升

动态 Schema 最大的麻烦之一,是同一个 Path 在不同批次里可能出现不同类型。

例如:

{"score": 10}
{"score": 10.5}
{"score": "unknown"}

前两种可以通过类型提升得到统一数值类型;第三种已经与数值类型发生冲突。Doris 会尽量做兼容类型扩展,无法形成稳定类型时可能回退到更通用的 JSONB 表达。这种回退意味着该路径后续的谓词下推、统计信息和索引能力都会受到影响。

所以,VARIANT 能吸收 Schema 漂移,但生产设计仍然需要治理关键路径。灵活性解决接入问题,稳定类型解决核心查询问题。 这正是 Schema Template 存在的原因,后面会详细展开。

2.2 哪些字段应该继续做普通列

使用 VARIANT 时,很多人容易走向另一个极端:既然动态 JSON 很方便,就把所有字段都放进一个 props VARIANT

生产环境里,更稳妥的做法通常是把字段分成三类:

字段类型 推荐存储 原因
分区列、分桶列、主键、时间、租户 ID 普通列 决定物理数据组织和高频裁剪
高频过滤、Join、排序、核心指标 普通列或 Schema Template 路径 类型和查询语义需要长期稳定
长尾属性、实验字段、设备扩展、事件扩展 VARIANT Schema 变化快,单行字段稀疏

例如埋点表里,event_timeuser_idevent_name 适合做实体列;设备固件版本、实验参数、推荐上下文、扩展标签可以放入 VARIANT。

这种设计能让 Doris 的分区、分桶、Prefix Index、CBO 统计信息和 Join 策略继续围绕稳定字段工作,同时让长尾字段保持灵活。


三、当 JSON Path 越来越多:Sparse Columns 解决长尾字段膨胀

默认 VARIANT 会为热点路径生成子列。官方文档当前默认的子列预算是每个 VARIANT 列 2048 个,由 variant_max_subcolumns_count 控制。

如果业务只有几十到几百个常见 Path,这个模式非常自然。问题出现在超宽 JSON:广告特征、车联网遥测、用户画像、可观测 Trace、风控特征等业务,Path 总数可能轻松达到几千甚至几万。

如果所有 Path 都变成独立物理子列,会带来新的成本:

  • Segment 元数据越来越大;
  • 写入阶段需要维护更多列结构;
  • 索引和统计信息增加;
  • Compaction 需要处理更多列;
  • 长尾字段几乎从来不查询,却长期占据独立列资源。

Doris 3.1 引入 Sparse Columns,解决的正是这一问题。

Sparse Columns
Sparse Columns

Sparse 的核心思想非常容易理解:

热点 Path 继续以独立子列存在;长尾 Path 进入共享稀疏存储。

比如一个用户画像系统拥有 20,000 个属性键,但真正被频繁分析的字段只有:

  • region
  • age
  • member_level
  • device_os
  • active_7d
  • pay_level
  • risk_level
  • channel

这几十、几百个 Path 继续保持列式子列,过滤和聚合可以走高效路径。其余上万个偶尔访问的字段进入 Sparse Storage。

3.1 用 variant_max_subcolumns_count 控制“热点预算”

可以把这个参数理解成:最多允许多少个路径享受独立列式子列待遇。

热点预算需要结合真实查询控制在合适范围。预算过大时,长尾字段重新回到超宽列问题;预算过小时,一些真正高频的字段也会进入 Sparse,导致读取成本提高。

调参前先做业务统计:

  1. 统计过去 7 天、30 天 SQL 中访问最多的 JSON Path;
  2. 计算访问频率和过滤选择性;
  3. 确认最常访问的几十到几百个字段;
  4. 根据这些字段数量设置初始预算;
  5. 再通过 Profile 和真实并发验证。

3.2 Sparse Sharding:长尾字段也不能都挤在一个物理列

如果长尾 Path 数量达到数万,一个共享 Sparse Column 也可能成为读放大热点。Doris 4.1 继续加入 Sparse Sharding:通过 variant_sparse_hash_shard_count 把长尾 Path 按 Hash 分散到多个物理 Sparse Column。

示意配置:

CREATE TABLE user_feature_wide (
    uid BIGINT,
    features VARIANT<
        'user_id' : BIGINT,
        'region' : STRING,
        properties(
            'variant_max_subcolumns_count' = '2048',
            'variant_sparse_hash_shard_count' = '32'
        )
    >
)
DUPLICATE KEY(uid)
DISTRIBUTED BY HASH(uid) BUCKETS 32
PROPERTIES (
    "storage_format" = "V3"
);

这类配置非常适合“Path 总量巨大、热点 Path 数量相对有限”的业务。官方 4.1 Release Notes 直接把车联网遥测、广告画像、用户特征、埋点日志和安全日志列为适用场景。


四、Schema Template:让关键 JSON Path 进入可治理状态

VARIANT 自动推断能够解决大量动态字段,但核心业务字段通常需要更强约束。

订单金额如果某批数据突然从数值变成字符串,分析结果会受到影响;user_id 如果一部分数据写成数字、一部分写成字符串,索引和 Join 都会变得复杂;日志里的 trace_idservice_namelevel 也往往需要统一类型和固定索引。

Schema Template 用来处理这些关键路径。

Schema Template
Schema Template

它允许你在 VARIANT 定义中声明部分 Path 的稳定类型,例如:

CREATE TABLE payment_event (
    event_time DATETIME(3),
    props VARIANT<
        'user_id' : BIGINT,
        'order_id' : BIGINT,
        'amount' : DECIMAL(18,2),
        'country' : STRING
    >
)
DUPLICATE KEY(event_time)
DISTRIBUTED BY HASH(event_time) BUCKETS 16;

Schema Template 有三个非常重要的工程价值。

4.1 类型稳定

关键 Path 不再完全依赖每批写入时的自动推断。这样可以降低类型漂移造成的查询不确定性,也便于数据质量校验。

4.2 索引稳定

Doris 当前支持对特定 VARIANT Path 配置 Path-specific Index。需要对某个关键字段长期建立倒排索引时,先把这个 Path 放入 Schema Template,会让类型和索引定义更加可控。

比如日志 message 需要全文检索,service_name 需要等值过滤,trace_id 需要精确查询,这些字段都很适合进入治理范围。

4.3 团队协作稳定

大型企业的日志、埋点和画像平台往往有几十个数据生产团队。如果完全依赖自动推断,同一路径命名、类型和含义可能逐渐失控。Schema Template 可以承担“关键字段契约”的角色:

  • 统一字段名;
  • 统一数据类型;
  • 统一业务含义;
  • 统一索引策略;
  • 统一上线评审。

这里有一个很重要的边界:模板只定义关键 Path。 如果把所有可能出现的几千个字段都写入模板,维护压力又会回到传统超宽 Schema。长尾字段继续交给 VARIANT 和 Sparse 处理即可。


五、Wide Table Storage Format V3:解决“打开文件之前就已经很贵”的问题

当表达到数百、数千甚至上万列时,查询慢的原因有时还没走到真正的数据扫描阶段。

Apache Doris 4.1 新增 Wide Table Storage Format V3。它解决的是一个非常底层的问题:旧 Segment 格式把大量 Column Metadata 集中放在 Footer 中。打开 Segment 时,系统需要先读取和反序列化 Footer。列越多、Segment 越多,这个“开门成本”越高。

即使 SQL 只访问两个字段,也可能先为几千列的元数据付费。

Storage Format V3
Storage Format V3

V3 的主要变化有三项。

5.1 Column Metadata 从 Footer 中拆出

V3 让 Footer 只保存轻量指针,真正的 Column Metadata 进入独立区域。查询打开 Segment 时,可以先读取一个很小的 Footer,然后按需加载实际访问列的元数据。

这项优化特别适合:

  • 数百到数千列传统宽表;
  • 拥有大量 VARIANT 子列的表;
  • 对象存储上的冷查询;
  • AI、车联网、遥测等随机读取较多的场景。

5.2 数值类型默认使用更直接的编码路径

V3 对 INTBIGINT 等数值类型默认采用 Plain Encoding,并与 LZ4/ZSTD 压缩配合,降低大批量读取中的解码 CPU 成本。

5.3 String / JSONB 使用新的 Binary Plain Encoding

新的字符串布局减少额外 offset 结构,进一步控制超宽场景下的空间和解析成本。

创建时显式指定:

CREATE TABLE event_v3 (
    id BIGINT,
    event_time DATETIME(3),
    attrs VARIANT
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 32
PROPERTIES (
    "storage_format" = "V3"
);

这里要特别注意版本边界:Storage Format V3 从 Apache Doris 4.1.0 开始支持。 当前官网在 2026-08-17 显示 Latest 为 4.1.3,Stable 为 4.0.8。因此,如果生产环境仍在 4.0.8,VARIANT、Sparse Columns、Schema Template 可以继续使用,但 V3 需要等升级到 4.1 系列后再评估。

官方文档提供了一个明确测试条件:7,000 列、10,000 个 Segment 的宽表中,Segment 打开时间从 65 秒降到 4 秒,打开阶段内存从 60 GB 降到 1 GB 以下。这个数字只能用于理解 V3 所解决的问题,生产系统不能直接照搬,需要用自己的列数、Segment 数量、存储介质和查询模式重新测试。


六、VARIANT、Sparse、Schema Template、V3 应该怎样组合

这四项能力分别解决不同层次的问题:

能力 解决的问题 主要收益 主要代价
VARIANT Schema 经常变化 动态接入 + Path 列式分析 写入时解析与子列维护
Sparse Columns Path 总数过大且长尾明显 控制物理子列数量 长尾 Path 查询成本更高
Schema Template 关键 Path 需要稳定类型和索引 类型、索引、治理稳定 模板需要版本管理
Storage Format V3 超宽表 Segment 元数据膨胀 降低打开延迟和内存 需要 Doris 4.1+

组合方式可以参考下面的决策树。

半结构化建模决策树
半结构化建模决策树

模式 A:普通事件日志

Path 数量几百,字段变化快,高频查询集中在几十个字段。

建议:

  • 固定列:时间、应用、服务、用户、事件名;
  • props VARIANT:扩展字段;
  • 关键日志字段建立倒排索引;
  • 4.1 新表优先评估 V3。

模式 B:广告特征 / IoT 遥测 / 超宽画像

Path 数量几千到几万,绝大多数列长期为 NULL,高频字段只有几十到几百个。

建议:

  • VARIANT;
  • 控制 variant_max_subcolumns_count
  • 开启 Sparse Columns;
  • 4.1 根据长尾读取压力设置 Sparse Sharding;
  • 4.1 配合 V3。

模式 C:支付 / 订单 / 核心设备属性

字段仍有变化,但少数路径必须保证类型稳定,且需要稳定索引。

建议:

  • 关键公共字段做实体列;
  • 扩展字段使用 VARIANT;
  • user_idamountcountrydevice_id 等进入 Schema Template;
  • 对关键 Path 配置索引;
  • 长尾明显时继续叠加 Sparse。

模式 D:经常返回完整 JSON 文档

Doris 4.1 的官方 VARIANT Workload Guide 还给出 DOC mode。它更偏向写入效率和整文档返回:写入阶段先保留完整文档结构,子列化可以延迟到后续 Compaction。

DOC mode 与 Sparse Columns 的目标不同,当前 4.1 文档明确要求两者二选一。Day 14 把 DOC mode 放在扩展内容中,核心原因是整套课程的主线更偏向分析、过滤、聚合和索引;如果你的实际系统有大量 SELECT props 返回完整文档的请求,DOC mode 值得单独做 POC。


登录后可阅读本文完整内容。