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

前六天,我们已经完成 Doris 的定位、架构、部署、SQL、数据类型与函数体系。今天进入表设计中影响最深的一层:数据模型。
在 Doris 中,数据模型规定了一个非常具体的问题:当相同 Key 再次写入时,存储引擎究竟要保留全部记录、按函数合并指标,还是只保留一个最新状态。 这项选择会持续影响数据正确性、写入成本、查询路径、更新能力、存储规模和后续迁移方式。
很多项目在建表时先讨论分区、分桶、索引和副本,数据模型只凭经验随手选择。这样的顺序容易把风险埋进底层。索引可以重建,物化视图可以增删,部分表属性也能调整;数据模型在建表时确定,无法通过一次简单的 ALTER TABLE 直接切换。模型选错以后,通常要新建目标表、回灌历史数据、追平增量、完成对账,再执行原子切换。
因此,Day 7 的学习目标可以归纳为一句话:
先确定相同业务 Key 的写入语义,再设计查询性能。
本日学习目标
完成本篇后,你应当能够:
- 清楚说明
DUPLICATE KEY、AGGREGATE KEY、UNIQUE KEY的长期存储语义; - 解释三种模型中 Key 分别承担排序、聚合粒度和主键唯一性等职责;
- 为日志明细、固定口径指标、订单当前状态和 CDC 同步选择合适模型;
- 理解 Unique Key 的 Merge-on-Write、Delete Bitmap、部分列更新和 Sequence Column;
- 识别 Aggregate Key 中粒度遗漏、
COUNT(*)误读、AVG 设计错误和REPLACE顺序风险; - 建立模型与排序键、分区、分桶、物化视图、行存列之间的协同关系;
- 独立完成三张实验表,并用相同输入验证三种不同结果;
- 输出一份可以进入架构评审的《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 的处理规则。规则会在多个环节持续生效:
- 批次写入阶段:同一批数据内部可能先完成排序、去重或预聚合;
- 版本发布阶段:一次导入形成新的 Rowset 和可见版本;
- Compaction 阶段:多个 Rowset 合并时继续执行模型规则;
- 查询阶段:读取结果必须符合模型定义,例如 Aggregate 做最终聚合,Unique 过滤旧版本;
- 更新与删除阶段:是否支持 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(...) 中的列负责排序,允许重复。

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 的排序列控制在三个以内。排序键选择可以遵循以下顺序:
- 高频、稳定、选择性较好的过滤列;
- 时间范围列常放在前部,便于连续数据裁剪;
- 低基数列是否靠前,要结合查询组合和数据聚集效果验证;
- 业务主键只有在高频点查时才具备靠前价值;
- 宽字符串会消耗前缀索引字节,应控制长度和位置;
- 分区列、排序列、分桶列可以不同,各自解决不同问题。
一个常见误区是把 MySQL 主键直接放到第一排序位。事件表的 event_id 虽然唯一,绝大多数查询可能按日期、服务和事件类型过滤。此时按 event_id 排序很难形成有效数据局部性。
6. 明细与聚合可以并存
Duplicate 基表负责保留事实,物化视图负责加速固定口径。例如基表存订单事件,异步物化视图按天、地区和渠道计算订单数、金额和用户数。这样可以兼顾追溯与报表性能。
需要注意,物化视图解决查询加速,无法改变基表的写入语义。业务要求主键 Upsert 时,Unique Key 仍然更直接。
三、Aggregate Key:用稳定粒度换取存储与查询收益
Aggregate Key 适合固定粒度的预聚合数据。Key 列定义分组粒度,Value 列逐一声明聚合函数。相同 Key 在写入、后台 Compaction 和查询阶段持续合并。

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_count 与 amount_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 大致经历以下步骤:
- BE 将批次写入 MemTable,并按 Key 排序;
- 通过 Segment 主键索引查找旧行所在的 Rowset、Segment 和 RowID;
- 在 Delete Bitmap 中标记旧行;
- 如配置 Sequence Column,比较新旧版本顺序;
- 将新完整行写入新 Rowset;
- 事务提交后发布版本,查询开始读取新状态;
- 旧数据继续留在物理文件中,后续由 Compaction 回收。

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. 内部过程
部分列更新仍然要生成完整的新行:
- 查找主键对应的历史记录;
- 读取输入中缺失的旧字段;
- 合并新字段和旧字段;
- 写入完整新行;
- 使用 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 的配置方式如下:
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 的处理规则。完整表设计还要继续回答四个问题:
- 排序键:数据在 Segment 中按照什么顺序组织;
- 分区:哪些数据可以通过时间或枚举范围快速裁剪和独立管理;
- 分桶:数据如何映射到 Tablet,实现并行读写与负载均衡;
- 物化视图:哪些高频查询值得预计算或改变排序路径。
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,根据精度要求选择;
- 平均值拆为总量与样本数;
- 重放数据要具备幂等或去重机制,防止重复累计。
十、模型选型决策树与评审矩阵

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

表设计评审清单
提交 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 = 2,amount_sum = 240.00,max_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 更小,无法覆盖当前版本。

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?
答案要点: 保存 SUM 与 COUNT 两个可加指标,查询时计算 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 官方文档校验当前行为: