第 07 关 · ★★

三大数据模型

Duplicate、Aggregate、Unique Key:建模选错,事倍功半。

已点亮 · 最佳 分

三大数据模型:Duplicate、Aggregate 与 Unique Key

先确定相同业务 Key 的写入语义,再设计查询性能

三大数据模型Merge-on-Write部分列更新Sequence Column
1 / 28

Day 7|Doris 三大数据模型:Duplicate、Aggregate 与 Unique Key

Day 7 学习总览
Day 7 学习总览

前六天,我们已经完成 Doris 的定位、架构、部署、SQL、数据类型与函数体系。今天进入表设计中影响最深的一层:数据模型

在 Doris 中,数据模型规定了一个非常具体的问题:当相同 Key 再次写入时,存储引擎究竟要保留全部记录、按函数合并指标,还是只保留一个最新状态。 这项选择会持续影响数据正确性、写入成本、查询路径、更新能力、存储规模和后续迁移方式。

很多项目在建表时先讨论分区、分桶、索引和副本,数据模型只凭经验随手选择。这样的顺序容易把风险埋进底层。索引可以重建,物化视图可以增删,部分表属性也能调整;数据模型在建表时确定,无法通过一次简单的 ALTER TABLE 直接切换。模型选错以后,通常要新建目标表、回灌历史数据、追平增量、完成对账,再执行原子切换。

因此,Day 7 的学习目标可以归纳为一句话:

先确定相同业务 Key 的写入语义,再设计查询性能。

本日学习目标

完成本篇后,你应当能够:

  1. 清楚说明 DUPLICATE KEYAGGREGATE KEYUNIQUE KEY 的长期存储语义;
  2. 解释三种模型中 Key 分别承担排序、聚合粒度和主键唯一性等职责;
  3. 为日志明细、固定口径指标、订单当前状态和 CDC 同步选择合适模型;
  4. 理解 Unique Key 的 Merge-on-Write、Delete Bitmap、部分列更新和 Sequence Column;
  5. 识别 Aggregate Key 中粒度遗漏、COUNT(*) 误读、AVG 设计错误和 REPLACE 顺序风险;
  6. 建立模型与排序键、分区、分桶、物化视图、行存列之间的协同关系;
  7. 独立完成三张实验表,并用相同输入验证三种不同结果;
  8. 输出一份可以进入架构评审的《Doris 数据模型选型与表设计评审清单》。

实验前提

本篇沿用 Day 5 搭建的单 FE、单 BE 学习环境,示例使用 replication_num = 1。生产集群需按照高可用和容灾要求设置副本。

版本口径

截至 2026-08-16,Apache Doris 官网将 4.1.3 标记为 Latest,将 4.0.8 标记为 Stable。Unique Key 自 2.1 起默认采用 Merge-on-Write。本文以 4.x 的 MoW 行为为主线,MoR 仅用于理解历史机制和特殊兼容场景。灵活部分列更新自 3.1.0 起支持同一批次内每行更新不同列。


一、数据模型是一份写入语义合同

Doris 的每张 OLAP 表都要选择一种模型:

  • DUPLICATE KEY:保留每一行;
  • AGGREGATE KEY:相同 Key 的 Value 列按聚合函数折叠;
  • UNIQUE KEY:每个主键保留一个可见版本。

这三种模型共用 Doris 的列式存储、分区、分桶、Tablet、Rowset、Segment、索引和 MPP 查询引擎。它们的核心差异集中在相同 Key 的处理规则。规则会在多个环节持续生效:

  1. 批次写入阶段:同一批数据内部可能先完成排序、去重或预聚合;
  2. 版本发布阶段:一次导入形成新的 Rowset 和可见版本;
  3. Compaction 阶段:多个 Rowset 合并时继续执行模型规则;
  4. 查询阶段:读取结果必须符合模型定义,例如 Aggregate 做最终聚合,Unique 过滤旧版本;
  5. 更新与删除阶段:是否支持 Upsert、部分列更新和删除标记,取决于模型能力。

1. Key 在三种模型中的含义

模型 Key 的主要含义 相同 Key 再次写入 查询看到的结果
Duplicate Key 排序键,可重复 追加新行 所有明细行
Aggregate Key 聚合分组键,同时也是排序键 按 Value 列聚合函数折叠 每个聚合 Key 的合并结果
Unique Key 主键唯一性,同时也是排序键 新版本替换旧版本 每个主键一行当前状态

这里有一个容易混淆的地方:Key 既参与物理排序,又可能承担业务语义。 Duplicate Key 的 Key 只决定排序;Aggregate Key 的 Key 定义聚合粒度;Unique Key 的 Key 定义主键。排序列会进一步影响前缀索引和数据局部性,所以模型选择与查询设计天然关联。

2. 同一批数据进入三种模型后的结果

假设写入以下三行:

user_id=1001, amount=100, status=PAID
user_id=1001, amount=50,  status=SHIPPED
user_id=1002, amount=80,  status=PAID

同一批业务数据在三种模型中的结果
同一批业务数据在三种模型中的结果

结果会出现明显差异:

  • Duplicate 表保留三行,1001 的两次事实都可追溯;
  • Aggregate 表若以 user_id 为 Key、amount 使用 SUM,会得到 1001 → 150
  • Unique 表以 user_id 为主键时,只保留 1001 的一个可见版本,覆盖顺序由加载顺序或 Sequence Column 决定。

模型选择因此不能从“哪一种速度最快”开始。先回答以下问题更加稳妥:

  • 业务是否需要保留原始明细?
  • 同一业务对象会不会持续更新?
  • 更新到达顺序是否可能乱序?
  • 指标口径是否固定且满足可合并性?
  • 数据能否在写入时永久失去明细?
  • 查询主要读取当前状态、原始事件,还是固定汇总?

建表前先区分事实、状态与指标

模型讨论很容易陷入 DDL 参数,业务数据的本质更值得先确认。可以把常见数据分为三类:

  • 事实事件记录已经发生的动作,例如支付成功、页面点击、设备告警和风控命中。后续状态变化不会抹去这条事实,完整时间线通常进入 Duplicate;
  • 当前状态描述某个实体此刻的有效值,例如订单状态、商品价格、用户等级和设备在线信息。查询希望直接获得每个主键的一行结果,通常进入 Unique;
  • 汇总指标表达固定粒度上的数值,例如分钟订单数、渠道成交额和广告曝光量。只要指标具备稳定的可合并性,就可以进入 Aggregate。

还要继续回答可逆性问题。Duplicate 保存全部输入,指标口径变化后仍可重新计算;Aggregate 会永久压缩到既定粒度,粒度外明细无法从表内恢复;Unique 保存当前逻辑状态,旧版本不会作为业务历史向查询暴露。财务、审计、风控解释和客户申诉通常需要长期追溯,因此状态表和汇总表之外,仍要保留一份可核查的事实层。

重试与乱序也会改变模型结果。Duplicate 会保存重复消费产生的记录;Aggregate 中可加指标可能被再次累加;Unique 会按 Key 裁决版本,Sequence 决定旧消息能否覆盖新状态。模型评审需要与幂等策略同时完成,明确事件唯一 ID、导入 Label、源端版本、补数范围和重复消费后的期望结果。

最后评估成本落点。Duplicate 让写入保持轻量,容量与聚合计算留给存储和查询;Aggregate 把固定计算前移到写入和 Compaction;Unique MoW 在写入阶段维护主键索引和 Delete Bitmap,换取稳定的状态查询。真实选型应同时压测写入、查询和读写混合负载。

二、Duplicate Key:原始事实层与自由分析的常用起点

Duplicate Key 是 Doris 的默认表模型。它不执行去重和聚合,即使两行内容完全相同,也会完整保留。DUPLICATE KEY(...) 中的列负责排序,允许重复。

Duplicate Key:原始事实层与自由分析
Duplicate Key:原始事实层与自由分析

1. 适用场景

Duplicate Key 很适合以下数据:

  • 应用访问日志、错误日志、审计日志;
  • 用户点击、曝光、浏览、停留等行为事件;
  • 交易流水、资金流水、设备遥测等需要逐条追溯的事实;
  • 上游事件已不可变,后续主要进行多维分析的数据;
  • 维度和查询口径仍在快速变化的早期数据集。

这类数据的共同特征是:每一行都具有独立事实价值。同一个用户一天点击十次,十行都应存在;同一个订单经历多个状态事件,事件表也应保留完整变化轨迹。

2. 建表示例

CREATE DATABASE IF NOT EXISTS doris_day7;
USE doris_day7;

CREATE TABLE event_log_dup (
    event_time DATETIME(3) NOT NULL,
    event_type VARCHAR(32) NOT NULL,
    service    VARCHAR(64) NOT NULL,
    event_id   BIGINT      NOT NULL,
    user_id    BIGINT      NOT NULL,
    amount     DECIMAL(18, 2) NULL,
    detail     STRING      NULL
)
DUPLICATE KEY(event_time, event_type, service)
DISTRIBUTED BY HASH(user_id) BUCKETS 4
PROPERTIES (
    "replication_num" = "1"
);

这里的 event_time, event_type, service 构成排序键。它们不保证唯一,也不负责去重。选择这些列的原因应来自查询模式,例如大量查询都带时间范围、事件类型和服务名过滤。

3. Duplicate Key 为什么适合作为明细层

写入路径短

新数据可以持续追加。存储引擎不需要查找历史主键,也不需要为旧行维护 Delete Bitmap。对于高吞吐日志和事件流,这条路径通常更轻。

原始事实完整

数据团队可以在后续重新定义指标、修正维表、回放算法、审计异常。早期业务口径经常变化,保留明细能降低“指标定义错误后无法重算”的风险。

查询维度自由

数据完整保留后,可以按用户、地区、渠道、设备、时间、商品等任意组合聚合。固定口径热点查询可以通过同步物化视图、异步物化视图、倒排索引、Bloom Filter 和合理排序键继续加速。

4. Duplicate Key 的成本

第一项成本来自存储。数据量会随着事件数量持续增长,重复记录也会保留。第二项成本来自查询。每次聚合需要从明细计算,热点报表如果长期扫描大量历史数据,应增加预计算层。第三项成本来自“当前状态”语义:同一订单出现十条状态事件时,查询当前状态需要窗口函数、聚合或额外状态表,复杂度高于 Unique Key。

5. 排序键如何选择

官方 4.x 文档通常建议 Duplicate Key 的排序列控制在三个以内。排序键选择可以遵循以下顺序:

  1. 高频、稳定、选择性较好的过滤列;
  2. 时间范围列常放在前部,便于连续数据裁剪;
  3. 低基数列是否靠前,要结合查询组合和数据聚集效果验证;
  4. 业务主键只有在高频点查时才具备靠前价值;
  5. 宽字符串会消耗前缀索引字节,应控制长度和位置;
  6. 分区列、排序列、分桶列可以不同,各自解决不同问题。

一个常见误区是把 MySQL 主键直接放到第一排序位。事件表的 event_id 虽然唯一,绝大多数查询可能按日期、服务和事件类型过滤。此时按 event_id 排序很难形成有效数据局部性。

6. 明细与聚合可以并存

Duplicate 基表负责保留事实,物化视图负责加速固定口径。例如基表存订单事件,异步物化视图按天、地区和渠道计算订单数、金额和用户数。这样可以兼顾追溯与报表性能。

需要注意,物化视图解决查询加速,无法改变基表的写入语义。业务要求主键 Upsert 时,Unique Key 仍然更直接。


三、Aggregate Key:用稳定粒度换取存储与查询收益

Aggregate Key 适合固定粒度的预聚合数据。Key 列定义分组粒度,Value 列逐一声明聚合函数。相同 Key 在写入、后台 Compaction 和查询阶段持续合并。

Aggregate Key 的三阶段聚合
Aggregate Key 的三阶段聚合

1. 建表示例

CREATE TABLE metric_1m_agg (
    metric_date   DATE          NOT NULL,
    metric_minute DATETIME      NOT NULL,
    region        VARCHAR(32)   NOT NULL,
    channel       VARCHAR(16)   NOT NULL,
    event_count   BIGINT        SUM DEFAULT "0",
    amount_sum    DECIMAL(20,2) SUM DEFAULT "0",
    max_latency   BIGINT        MAX DEFAULT "0",
    last_update   DATETIME      REPLACE DEFAULT "1970-01-01 00:00:00"
)
AGGREGATE KEY(metric_date, metric_minute, region, channel)
DISTRIBUTED BY HASH(region, channel) BUCKETS 4
PROPERTIES (
    "replication_num" = "1"
);

这张表的业务粒度是:日期 × 分钟 × 地区 × 渠道。同一粒度内,event_countamount_sum 持续累加,max_latency 保留最大值,last_update 使用后写入值替换旧值。

2. 常用聚合函数

聚合方式 典型含义 使用提醒
SUM 可加指标累计 适合金额、次数、时长总量
MAX 保留最大值 适合峰值、最高水位
MIN 保留最小值 适合最低价、最早时间等
REPLACE 使用后写入值 结果受版本和写入顺序影响
REPLACE_IF_NOT_NULL 新值非空时替换 可用于多流补列,无法把字段更新为 NULL
BITMAP_UNION 精确集合并集 适合精确 UV、留存、人群集合
HLL_UNION HLL 状态合并 适合近似 UV 和趋势分析

3. 聚合粒度是第一道正确性边界

假设业务希望按 date + region + channel 统计 PV。建表时遗漏 channel,APP 与 Web 数据会永久折叠。查询层已经无法恢复渠道明细。

因此,Aggregate Key 设计需要先写出完整的粒度句子:

每一行代表【时间粒度】下【维度 A × 维度 B × 维度 C】的一组指标。

然后逐项检查:

  • Key 是否包含所有需要长期区分的维度;
  • 维度是否可能在未来进入报表筛选;
  • 指标能否跨批次、跨 Rowset、跨分区安全合并;
  • 原始明细是否在其他层保留;
  • 迟到数据、重放数据和重复消费会不会造成重复累计。

4. COUNT(*) 为什么可能产生误解

Aggregate 表的物理行表示聚合后的粒度行。COUNT(*) 统计的是当前聚合结果行数,无法直接代表原始事件数。若要统计事件量,应准备 event_count BIGINT SUM,每条原始事件写入 1,查询时读取或汇总 event_count

5. AVG 应拆为 SUM 与 COUNT

平均值通常不具备简单的二次平均性质。两个分组的平均值直接求平均,只有在样本数相同的条件下才正确。稳妥设计是存储:

amount_sum   DECIMAL(20,2) SUM,
event_count  BIGINT        SUM

查询时计算:

SELECT amount_sum / NULLIF(event_count, 0) AS avg_amount
FROM metric_1m_agg;

这样在分钟汇总到小时、小时汇总到天时仍然可以得到正确结果。

6. REPLACE 与顺序控制

REPLACE 表示后写入值覆盖此前值。它不等同于“业务时间最新”。网络抖动可能让旧事件后到,旧值就可能覆盖新值。对顺序敏感的当前状态数据,Unique Key 配合 Sequence Column 更清晰。

7. REPLACE_IF_NOT_NULL 的能力与边界

Aggregate 表可以把需要补列的字段声明为 REPLACE_IF_NOT_NULL,不同数据流只写自己负责的字段,空值不会覆盖已有值。这种方式写入轻,查询时需要完成聚合。官方文档指出,在该类部分更新模式下,典型聚合查询成本高于 Unique Key MoW;同时字段无法通过这种语义更新为 NULL

因此,REPLACE_IF_NOT_NULL 适合:

  • 写吞吐优先;
  • 查询延迟要求相对宽松;
  • 字段不需要显式更新为 NULL;
  • 业务能够接受 Aggregate 模型的最终聚合成本。

四、Unique Key:当前状态、CDC 与主键更新

Unique Key 为每个主键维护一条可见记录。新数据携带已存在的 Key 时,会执行 Upsert;不存在时插入新行。它适合订单状态、用户画像、设备状态、实时维表、账户快照和数据库 CDC 同步。

1. 建表示例

CREATE TABLE order_current_uni (
    order_id   BIGINT        NOT NULL,
    user_id    BIGINT        NOT NULL,
    status     VARCHAR(32)   NOT NULL,
    amount     DECIMAL(18,2) NOT NULL,
    city       VARCHAR(64)   NULL,
    updated_at DATETIME(6)   NOT NULL
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 4
PROPERTIES (
    "replication_num" = "1",
    "enable_unique_key_merge_on_write" = "true",
    "function_column.sequence_col" = "updated_at",
    "store_row_column" = "true"
);

order_id 同时承担主键唯一性和排序职责。updated_at 作为 Sequence Column,帮助 Doris 在乱序到达时保留业务时间更大的状态。store_row_column 为宽表点查和部分列更新提供行式副本,可减少读取完整行时的 IOPS。

2. Merge-on-Write 的工作过程

Doris 2.1 起,Unique Key 默认使用 Merge-on-Write。一次 Upsert 大致经历以下步骤:

  1. BE 将批次写入 MemTable,并按 Key 排序;
  2. 通过 Segment 主键索引查找旧行所在的 Rowset、Segment 和 RowID;
  3. 在 Delete Bitmap 中标记旧行;
  4. 如配置 Sequence Column,比较新旧版本顺序;
  5. 将新完整行写入新 Rowset;
  6. 事务提交后发布版本,查询开始读取新状态;
  7. 旧数据继续留在物理文件中,后续由 Compaction 回收。

Unique Key:Merge-on-Write 与 Merge-on-Read
Unique Key:Merge-on-Write 与 Merge-on-Read

MoW 把版本裁决前移到写入阶段。查询时根据 Delete Bitmap 跳过旧行,无需对同一 Key 的多个版本做实时合并,因此适合查询延迟敏感、点查较多和 CDC 持续更新的场景。

3. Merge-on-Read 的历史作用

MoR 的写入路径更轻:新版本直接追加,查询时再合并同一 Key 的多条记录。这个机制节省部分写入处理,却会把成本留给每次读取。4.x 仍可通过 enable_unique_key_merge_on_write = false 选择 MoR,但新项目通常应优先评估 MoW。只有在明确的写入瓶颈、兼容需求和完整压测证据下,MoR 才具备讨论价值。

4. 全行 Upsert

通过 Stream Load、Routine Load、Flink Connector 或 INSERT INTO 写入 Unique 表时,默认执行全行 Upsert:

  • 主键不存在:插入;
  • 主键已存在:新行覆盖旧行。

全行 Upsert 要求输入能提供完整字段。CDC 捕获完整 Before/After Image 时,这种方式最直接。


五、部分列更新:便利背后的读写放大

业务事件经常只修改一个字段,例如订单从 PAID 变为 SHIPPED,金额、用户、城市保持不变。Unique Key MoW 支持部分列更新,输入只需携带完整主键和待更新字段。

SET enable_unique_key_partial_update = true;

INSERT INTO order_current_uni
    (order_id, status, updated_at)
VALUES
    (1001, 'SHIPPED', '2026-08-16 10:01:00.000000');

通过 Stream Load 时,可以使用:

curl --location-trusted -u root: \
  -H "partial_columns:true" \
  -H "column_separator:," \
  -H "columns:order_id,status,updated_at" \
  -T update.csv \
  http://127.0.0.1:8030/api/doris_day7/order_current_uni/_stream_load

部分列更新的写入路径
部分列更新的写入路径

1. 内部过程

部分列更新仍然要生成完整的新行:

  1. 查找主键对应的历史记录;
  2. 读取输入中缺失的旧字段;
  3. 合并新字段和旧字段;
  4. 写入完整新行;
  5. 使用 Delete Bitmap 隐藏旧版本。

这会产生读放大和写放大。更新列只占宽表很小一部分时,系统仍需读取历史字段并写出完整新版本;实际放大量会受到列宽、压缩、缓存命中、批次大小和磁盘性能影响。生产验收应直接记录 BE 读取字节、写入字节、IOPS 和每批延迟。

2. 性能建议

  • 使用 NVMe SSD 或高性能云盘;
  • 宽表考虑启用 store_row_column = true
  • 把单行高频更新合并为批次;
  • 高吞吐更新优先使用 Stream Load、Routine Load 或 Flink Connector;
  • 监控写入延迟、磁盘 IOPS、Rowset 数量和 Compaction Score;
  • 避免在 Schema Change 期间提交部分列更新;
  • 同步物化视图与部分更新存在能力限制,建表前应核对当前版本文档。

3. 固定列更新与灵活列更新

传统部分列更新要求同一批次中的所有行更新同一组字段。Doris 3.1.0 起支持灵活部分列更新,每一行可以更新不同字段。该能力要求:

  • 表为 Unique Key MoW;
  • 开启 enable_unique_key_skip_bitmap_column = true
  • 加载数据采用 JSON;
  • 加载模式设置为 UPDATE_FLEXIBLE_COLUMNS
  • 每一行都包含完整 Key;
  • Variant 列和同步物化视图等限制需要提前核验。

灵活更新很适合 Debezium、Flink CDC 等只输出变更字段的链路。它增加了表属性和加载参数,生产使用前要做并发更新、异常重试、BE 重启和数据对账测试。

4. 新 Key 行为控制

部分列更新遇到不存在的 Key 时,可以通过 partial_update_new_key_behavior 控制:

  • ERROR:只允许更新已存在记录;
  • APPEND:允许插入新记录,缺失字段使用默认值、NULL,或在不满足约束时失败。

这个参数应由业务语义决定。账户余额增量通常适合 ERROR,用户画像补录场景可能接受 APPEND


六、Sequence Column:让乱序 CDC 保留正确状态

实时链路中,业务发生顺序和网络到达顺序可能不同。订单在 10:00 变为 PAID,10:01 变为 SHIPPED;由于重试或网络延迟,PAID 事件反而更晚抵达 Doris。单纯依赖到达顺序会让旧值覆盖新值。

Sequence Column 与删除语义
Sequence Column 与删除语义

Sequence Column 的配置方式如下:

PROPERTIES (
    "function_column.sequence_col" = "updated_at"
)

当相同主键的多个版本发生竞争时,Doris 比较 Sequence 值,保留更大的版本。Sequence 通常来自:

  • 数据库 Binlog 提交时间;
  • 业务版本号;
  • 事件时间与递增序列的组合;
  • 上游 CDC 能稳定提供的单调字段。

1. Sequence 设计要点

  • 字段必须能表达同一主键下的先后关系;
  • 时间戳精度要能区分高频更新;
  • 多源系统时间可能漂移,统一版本号更可靠;
  • 同一批次内出现相同 Key、相同 Sequence、不同 Value 时,结果可能受到加载顺序影响;
  • 重放、补数、回滚场景需要明确 Sequence 生成策略;
  • 删除事件也应携带可比较的版本信息。

2. 多流更新与 Sequence Mapping

一张用户宽表可能由多条数据流并行更新:行为流更新 last_login,交易流更新 total_amount,标签流更新 risk_level。单一全局 Sequence 会让不同流相互阻挡。4.x 提供 Sequence Mapping,可以为不同字段组指定独立 Sequence 列,使各数据流按自己的版本推进。

这项能力适合实时宽表组装,配置复杂度也更高。建议先确定每条流负责的字段边界、回放规则和冲突处理,再设计映射。


七、删除:查询隐藏与物理回收分成两个阶段

Unique Key 支持通过加载路径写入隐藏列 __DORIS_DELETE_SIGN__ = 1,也支持 SQL DELETE。删除发生后,查询会隐藏对应主键,物理文件不会立即消失。后台 Compaction 在后续合并中回收旧记录与删除标记占用的空间。

这套机制带来几个工程结论:

  • 删除完成后的逻辑结果可以很快生效;
  • 磁盘空间回收存在时间差;
  • 大批量删除会增加 Delete Bitmap 和 Compaction 压力;
  • 高频逐行删除应转换为批量加载删除标记;
  • 删除后立即观察磁盘空间,容易得到错误判断;
  • 生产监控要同时查看可见数据、Rowset、Compaction 和磁盘使用情况。

对于 Duplicate 与 Aggregate 表,Doris 也支持条件 DELETE,但更新和删除能力受模型语义与条件限制。持续主键级变更仍应优先使用 Unique Key。


八、模型、排序键、分区、分桶和物化视图如何协同

数据模型只回答相同 Key 的处理规则。完整表设计还要继续回答四个问题:

  1. 排序键:数据在 Segment 中按照什么顺序组织;
  2. 分区:哪些数据可以通过时间或枚举范围快速裁剪和独立管理;
  3. 分桶:数据如何映射到 Tablet,实现并行读写与负载均衡;
  4. 物化视图:哪些高频查询值得预计算或改变排序路径。

1. Duplicate Key

  • Key 只承担排序;
  • 常见做法是时间列加高频过滤维度;
  • 明细保留后,可用物化视图补充固定口径;
  • 高基数等值查询可考虑 Bloom Filter 或倒排索引;
  • 事件主键可作为分桶键,排序位置仍按查询模式决定。

2. Aggregate Key

  • Key 同时定义粒度和排序;
  • Key 过少会错误合并,过多会降低预聚合收益;
  • 分区列通常应进入 Key,确保粒度与生命周期一致;
  • Value 列的聚合函数要满足跨批次、跨 Rowset 的合并语义;
  • 固定报表可以直接读取聚合表,灵活分析仍需明细层支撑。

3. Unique Key

  • Key 同时定义主键和排序;
  • 主键很宽时,索引、写入和存储成本都会增加;
  • 更新与查询经常按完整主键进行,分桶键通常与主键保持较强关联;
  • 分区列必须满足 Unique 模型的唯一性约束,具体规则按当前版本文档核验;
  • 宽表点查可以配合行存列与 Prepared Statement;
  • 查询维度与主键方向差异较大时,可增加物化视图、倒排索引或独立服务表。

4. 一张表承载全部语义往往会变得沉重

订单域经常同时需要:

  • 完整状态变化轨迹;
  • 当前订单状态;
  • 分钟级经营指标。

把三类需求压进一张表,会让写入语义、排序、更新和报表口径相互牵制。拆成事件明细表、当前状态表和汇总指标表,职责更加清晰。

三类核心表:事件、指标与当前状态
三类核心表:事件、指标与当前状态


九、三类业务案例的完整选型

案例一:应用埋点与行为日志

需求特征:每天数十亿事件;字段持续增加;需要回放、漏斗、路径、留存和临时分析。

推荐模型:Duplicate Key。

原因:每次事件都有独立价值,维度和指标仍会变化。可以按日期分区,按用户或设备分桶,排序键优先使用时间和稳定过滤列。热点漏斗和留存指标通过异步物化视图或独立汇总表加速。

案例二:订单当前状态与 MySQL CDC

需求特征:订单会持续更新;查询只关心当前状态;上游存在插入、更新和删除;网络可能乱序。

推荐模型:Unique Key MoW。

关键设计

  • 主键与上游业务主键保持一致;
  • Sequence 使用 Binlog 提交时间或稳定版本号;
  • 删除映射到 __DORIS_DELETE_SIGN__
  • 高频更新走 Flink/Routine Load/Stream Load;
  • 宽表部分更新评估行存列和 SSD;
  • 上线前验证重试、乱序、重复消费、断点恢复和全量增量对账。

案例三:分钟级经营指标

需求特征:粒度固定;数据持续累加;报表查询频繁;原始订单已经在明细层保留。

推荐模型:Aggregate Key。

关键设计

  • Key 明确到分钟、区域、渠道和业务单元;
  • 金额与次数使用 SUM
  • 峰值使用 MAX
  • UV 使用 Bitmap 或 HLL,根据精度要求选择;
  • 平均值拆为总量与样本数;
  • 重放数据要具备幂等或去重机制,防止重复累计。

十、模型选型决策树与评审矩阵

模型选型决策树
模型选型决策树

可以用以下顺序做初选:

  1. 相同业务 Key 再次出现时,是否需要保留每一条事实?需要则优先 Duplicate;
  2. 是否允许按照固定粒度永久合并?允许则评估 Aggregate;
  3. 是否要求每个主键只保留当前状态?要求则选择 Unique;
  4. 数据到达是否可能乱序?可能则配置 Sequence;
  5. 是否需要部分列更新?需要则评估 MoW、行存列、磁盘和批次;
  6. 查询是否长期依赖固定汇总?可以增加 Aggregate 表或物化视图;
  7. 模型仍难确定时,先保留明细事实,再用派生表承担稳定口径。

模型选型矩阵
模型选型矩阵

表设计评审清单

提交 DDL 评审前,至少回答以下问题:

检查项 必须给出的答案
业务事实 一行代表什么业务对象或事件?
重复 Key 再次写入时保留、聚合还是覆盖?
原始明细 聚合或覆盖后,明细在哪里保留?
更新频率 每秒更新量、批次大小、单 Key 热度是多少?
顺序控制 是否乱序,Sequence 从哪里来?
删除语义 如何产生删除,多久要求物理回收?
查询模式 点查、时间范围、聚合、全文检索的比例是多少?
排序键 前三列为什么这样排列?
分区分桶 数据规模、Tablet 数量和扩容路径如何估算?
精度与口径 SUM、AVG、UV、COUNT 的业务定义是什么?
版本能力 当前 Stable 是否支持所需特性?
迁移方案 模型选错或业务变化时如何回灌与切换?

十一、模型转换:新建、回灌、追平、对账、切换

数据模型写入表元数据,无法通过简单 Schema Change 直接改变。常规迁移流程如下:

模型转换与上线切换
模型转换与上线切换

1. 新建目标表

按照新模型重新设计 Key、字段、分区、分桶、表属性和索引。

2. 全量回灌

INSERT INTO target_table
SELECT ...
FROM source_table;

模型变化会改变结果行数。Duplicate 转 Unique 时要确定哪一行胜出;Duplicate 转 Aggregate 时要完成正确聚合;Unique 转 Duplicate 时只能得到当前状态,历史版本无法凭空恢复。

3. 增量追平

全量回灌期间,源表仍可能持续写入。可以使用双写、CDC 或按时间水位追平增量。

4. 数据验收

至少完成:

  • 总行数、分区行数和主键数量核对;
  • 金额、次数、UV 等指标对账;
  • 随机抽样与边界样本验证;
  • 重复、乱序、删除、NULL 和迟到数据测试;
  • 关键 SQL 的 p50、p95、p99 对比;
  • 写入吞吐、磁盘、内存和 Compaction 压测。

5. 原子切换与回滚

在确认下游权限、视图、任务和应用连接已准备后,可以使用表替换能力完成切换。旧表应保留一段观察期,回滚脚本需要提前演练。


十二、生产验证:模型选型要通过真实负载证明

模型选择完成后,还需要用真实数据与真实 SQL 验证。只比较单条查询耗时,很难发现更新放大、乱序覆盖、重复消费和 Compaction 堆积。建议把验证拆成正确性、写入、查询、存储运维和故障恢复五组。

1. 正确性验证

为每种模型准备一组带有重复 Key、NULL、迟到、乱序、删除和重放的数据:

  • Duplicate 表核对每条事件都能追溯,重复加载是否符合业务预期;
  • Aggregate 表核对相同粒度的 SUM、MAX、Bitmap、HLL 结果,重点验证跨批次和跨天补数;
  • Unique 表核对主键唯一性、Sequence 胜出规则、部分更新补齐字段和删除后的可见性;
  • 对金额、次数、UV 等核心指标同时做全量总账与分区抽样对账;
  • 设计一批故意错误的数据,确认过滤行、错误 URL 和任务状态能够被监控发现。

聚合表还要做“可重放性”测试。若上游任务重复消费同一批 SUM 数据,指标会再次累加。需要由上游保证幂等、使用可替换口径,或通过临时分区与原子覆盖完成重算。

2. 写入验证

至少记录以下指标:

  • 平均与峰值写入行数、字节数;
  • 单批大小、批次数量和事务提交频率;
  • p50、p95、p99 写入可见延迟;
  • Unique MoW 的主键查找耗时和部分更新延迟;
  • BE 磁盘 IOPS、吞吐、CPU 与内存;
  • Rowset 增长速度、Compaction Score 和失败重试数量。

高频小批写入容易制造大量 Rowset,降低读取效率并增加 Compaction 压力。测试应覆盖业务常态批次、峰值批次和异常重试风暴,避免只用一个大文件得出过于乐观的结论。

3. 查询验证

查询测试要覆盖冷缓存、热缓存和读写混合三种状态。对 Duplicate 表关注扫描行数、扫描字节和物化视图命中;对 Aggregate 表关注最终聚合开销;对 Unique 表关注点查、范围查询、Delete Bitmap 过滤和宽表全列读取。

统一记录:

  • 单并发与目标并发下的 p50、p95、p99;
  • EXPLAIN 中的分区、Tablet 与索引裁剪;
  • Query Profile 中 Scan、Join、Aggregation、Exchange 的耗时;
  • 查询内存峰值、Spill、网络传输和返回行数;
  • 更新高峰期间查询 SLA 是否稳定。

4. 存储与运维验证

同一份业务数据分别写入三种模型,可以观察压缩后存储、Rowset 数量、Segment 数量和 Compaction 负担。Aggregate 往往存储更小;Duplicate 保留事实,容量较大;Unique 还会维护主键索引和 Delete Bitmap。最终成本要以真实列宽、更新比例和查询模式为准。

5. 故障与恢复验证

在测试环境主动执行 BE 重启、加载重试、CDC 断点恢复和重复提交。检查 Label 幂等、事务可见性、Sequence 结果、删除状态和副本一致性。对于核心表,上线门禁应包含完整对账、持续压测、故障演练和回滚脚本四类证据。


十三、动手实验:用同一批输入验证三种模型

完整 SQL 已放在配套文件 sql/day07-three-models-lab.sql 中。下面展示实验核心。

1. Duplicate 表

INSERT INTO event_log_dup
    (event_time, event_id, user_id, event_type, service, amount, detail)
VALUES
('2026-08-16 10:00:00.000', 1, 1001, 'ORDER_STATUS', 'order', 120.00, 'PENDING'),
('2026-08-16 10:01:00.000', 2, 1001, 'ORDER_STATUS', 'order', 120.00, 'PAID');

SELECT event_id, user_id, detail
FROM event_log_dup
WHERE user_id = 1001
ORDER BY event_time;

预期结果:两条事件都存在。

2. Aggregate 表

INSERT INTO metric_1m_agg VALUES
('2026-08-16', '2026-08-16 10:00:00', '华东', 'APP', 1, 120.00, 30, '2026-08-16 10:00:10'),
('2026-08-16', '2026-08-16 10:00:00', '华东', 'APP', 1, 120.00, 45, '2026-08-16 10:00:20');

SELECT metric_date, metric_minute, region, channel,
       event_count, amount_sum, max_latency
FROM metric_1m_agg;

预期结果:event_count = 2amount_sum = 240.00max_latency = 45

3. Unique 表与乱序更新

INSERT INTO order_current_uni VALUES
(1001, 9001, 'PENDING', 120.00, 'Ningbo', '2026-08-16 10:00:00.000000'),
(1001, 9001, 'PAID',    120.00, 'Ningbo', '2026-08-16 10:01:00.000000'),
(1001, 9001, 'CREATED', 120.00, 'Ningbo', '2026-08-16 09:59:00.000000');

SELECT order_id, status, amount, updated_at
FROM order_current_uni
WHERE order_id = 1001;

预期结果:只返回一行,状态为 PAID。第三条记录到达更晚,但 Sequence 更小,无法覆盖当前版本。

重复 Key 的实验结果
重复 Key 的实验结果

4. 实验验收要求

  • 保存三张表的 SHOW CREATE TABLE
  • 保存每次写入 SQL 和查询结果;
  • 重复执行写入,观察幂等差异;
  • 在 Unique 表测试部分列更新;
  • 去掉 Sequence 后重复乱序实验,比较结果;
  • 使用 EXPLAIN 观察过滤和分桶裁剪;
  • 记录三张表的行数、存储量与查询耗时;
  • 总结每种模型在当前业务中的适用范围。

十四、常见错误与排查思路

错误一:Aggregate Key 粒度遗漏

表现:渠道、地区或业务线数据被合并,查询无法拆回。

处理:停止继续写入,确认正确粒度,新建目标表并从明细源重算。源数据只剩聚合结果时,遗漏维度无法恢复。

错误二:把 COUNT(*) 当作事件数

表现:Aggregate 表中的行数远小于原始事件量。

处理:增加 event_count SUM,从明细层重新回灌。

错误三:Unique Key 没有 Sequence

表现:网络重试后旧状态覆盖新状态,多个副本结果可能不稳定。

处理:为业务对象引入稳定版本字段,验证同批重复 Key、跨批乱序和重放场景。

错误四:部分列更新吞吐低

表现:磁盘 IOPS 高、写入延迟抖动、Compaction Score 上升。

处理:检查批次大小、SSD、行存列、Key 宽度、热 Key、Rowset 数量和同步物化视图限制。把高频单行写入合并为批量加载。

错误五:Duplicate 表排序键沿用业务主键

表现:时间范围查询扫描量大,前缀索引收益低。

处理:根据真实 SQL 统计高频过滤组合,建立新表或物化视图调整排序路径。

错误六:模型转换直接在线修改

表现:尝试 ALTER TABLE 修改模型失败,或对旧表进行高风险操作。

处理:采用新表回灌流程,保留回滚窗口和完整对账证据。


十五、Knowledge Check

1. Duplicate Key 中的 Key 主要承担什么职责?

A. 保证唯一性
B. 对 Value 求和
C. 数据排序
D. 控制事务隔离

答案:C。 相同 Key 可以重复,所有行都会保留。

2. 哪类场景优先选择 Aggregate Key?

A. 原始日志审计
B. 订单当前状态
C. 固定粒度分钟指标
D. 高频单行事务

答案:C。 前提是粒度稳定、指标可合并,并且明细已经在其他位置保留。

3. Unique Key MoW 为什么查询性能更稳定?

答案要点: 写入阶段已经完成旧版本定位和 Delete Bitmap 标记,查询直接过滤旧行,无需实时合并同一 Key 的多个版本。

4. Aggregate 表中如何正确保存 AVG?

答案要点: 保存 SUMCOUNT 两个可加指标,查询时计算 SUM / COUNT

5. 部分列更新为什么会产生读写放大?

答案要点: MoW 需要读取未提供的历史字段,补齐完整行,再写入新版本。

6. CDC 乱序更新应使用什么能力?

答案:Sequence Column。 多数据流独立更新不同字段时,还可以评估 Sequence Mapping。

7. REPLACE_IF_NOT_NULL 有什么重要限制?

答案要点: 新值为 NULL 时不会覆盖旧值,因此无法用该语义把字段主动更新为 NULL。

8. 数据模型能否通过普通 Schema Change 直接切换?

答案:不能。 需要新建目标表、回灌、追平、对账和切换。


十六、本日总结

今天建立了 Doris 表设计中最重要的写入语义框架:

  • Duplicate Key 保留全部事实,适合日志、事件和自由分析;
  • Aggregate Key 以固定粒度归并指标,适合预聚合报表;
  • Unique Key 维护主键当前状态,适合 CDC、订单、画像和实时维表;
  • MoW 将版本裁决放在写入阶段,4.x 项目通常优先采用;
  • 部分列更新降低上游组装成本,同时带来历史读取和完整行重写;
  • Sequence Column 负责乱序控制,Delete Sign 负责逻辑删除;
  • 排序键、分区、分桶、索引和物化视图需要在模型确定后继续协同设计;
  • 一类业务可以使用多张不同模型的表,各自承担明细、当前状态与汇总服务。

Day 8 将进入表结构的第二层:排序键、分区、分桶、副本与 Tablet 设计。届时我们会回答数据如何分布、一次查询能够裁剪多少数据、Tablet 数量怎样估算,以及扩容和高并发为什么会受到分桶设计影响。


官方资料基线

本文基于你提供的培训材料第 106–125、138–140、165–183 页进行重构,并以 Apache Doris 4.x 官方文档校验当前行为:

  1. Data Model
  2. Duplicate Key Model
  3. Aggregate Model
  4. Unique Key
  5. Data Update Overview
  6. Column Update
  7. Prefix Index and Sort Key
  8. Multi-Stream Updates for the Unique Model
  9. Apache Doris Download