Apache Doris 4.0.8 数据建模实战(第 2 篇):同一批订单写进三种模型,为什么得到三个答案

订单 9001 在 10:05 已经支付。网络恢复后,10:03 产生的取消事件最后到达 Doris。导入任务全部成功,报表却可能得到三种结果:三条历史、一个从未真实发生过的拼接状态,或者正确的已支付状态。

差异不在 SQL,也不在导入工具,而在建表时那一行 DUPLICATE KEYAGGREGATE KEYUNIQUE KEY

Doris 的 Key 模型不是数据如何摆放,而是同一个 Key 再次到达时,存储层必须执行什么业务语义。

三个事件足以让错误模型现形

同一个订单依次到达三次。第三条最晚到达 Doris,但它的业务时间早于第二条。

到达顺序 业务时间 状态 支付金额 含义
1 10:00 CREATED 100.00 创建订单
2 10:05 PAID 100.00 完成支付
3 10:03 CANCELLED 0.00 迟到的旧事件

真正的当前状态只能是 PAID。如果系统按到达顺序覆盖,最后会退回 CANCELLED;如果各列独立聚合,还会拼出业务上从未存在的一行。

下面四张表把这种差异直接暴露出来。为了方便单 BE 测试使用一个 Bucket 和一个副本,生产环境不要照搬这两个数值。

sql 复制代码
CREATE DATABASE IF NOT EXISTS doris_model_lab;
USE doris_model_lab;

CREATE TABLE order_event_dup (
    order_id BIGINT,
    update_time DATETIME,
    status VARCHAR(16),
    pay_amount DECIMAL(12, 2)
)
DUPLICATE KEY(order_id, update_time)
DISTRIBUTED BY HASH(order_id) BUCKETS 1
PROPERTIES ("replication_num" = "1");

CREATE TABLE order_state_agg (
    order_id BIGINT,
    update_time DATETIME MAX,
    status VARCHAR(16) REPLACE,
    pay_amount DECIMAL(12, 2) REPLACE
)
AGGREGATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 1
PROPERTIES ("replication_num" = "1");

CREATE TABLE order_state_unique_arrival (
    order_id BIGINT,
    update_time DATETIME,
    status VARCHAR(16),
    pay_amount DECIMAL(12, 2)
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 1
PROPERTIES (
    "replication_num" = "1",
    "enable_unique_key_merge_on_write" = "true"
);

CREATE TABLE order_state_unique_event (
    order_id BIGINT,
    update_time DATETIME,
    status VARCHAR(16),
    pay_amount DECIMAL(12, 2)
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 1
PROPERTIES (
    "replication_num" = "1",
    "enable_unique_key_merge_on_write" = "true",
    "function_column.sequence_col" = "update_time"
);

对四张表分别执行下面三条写入,每条保持为独立事务,顺序不能调整。<目标表> 依次替换为四个表名。

sql 复制代码
INSERT INTO <目标表> VALUES
(9001, '2026-08-27 10:00:00', 'CREATED', 100.00);

INSERT INTO <目标表> VALUES
(9001, '2026-08-27 10:05:00', 'PAID', 100.00);

INSERT INTO <目标表> VALUES
(9001, '2026-08-27 10:03:00', 'CANCELLED', 0.00);

预期结果不是简单的三选一。

查询结果 原因
order_event_dup 三条事件全部保留 Key 只负责排序,不负责唯一性
order_state_agg 10:05、CANCELLED、0.00 MAXREPLACE 分列执行,拼出不存在的行
order_state_unique_arrival 10:03、CANCELLED、0.00 没有 Sequence 时,后到版本覆盖先到版本
order_state_unique_event 10:05、PAID、100.00 同 Key 比较 update_time,业务版本更大的行胜出

这个实验揭示了一个比模型定义更重要的事实:Doris 能严格执行你声明的存储语义,但它不会替你判断那是不是正确的业务语义。

Duplicate Key 保存事实,不负责回答现在是什么

Duplicate Key 官方文档 明确规定 Key 列只作为排序列,相同 Key 的记录不会去重或聚合。它最适合不可变事件、日志、行为轨迹和审计明细。

它的优势是写入路径最短、原始信息不丢、任何新分析口径都能回到明细重算。代价也很直接:当前订单状态必须在查询时通过窗口函数、MAX_BY 类逻辑或上游计算得到,重复投递也不会自动消失。

把 CDC 订单直接落成 Duplicate Key 并不是一定错误。下面两种目标必须分开:

text 复制代码
需要完整变更历史 → Duplicate Key
需要每个订单的当前状态 → Unique Key + 可靠 Sequence

一张表同时承担审计历史与在线状态查询,往往才是问题来源。

Aggregate Key 聚合的是列,不是完整业务行

Aggregate Key 官方文档 规定相同 Key 的 Value 列按照各自声明的函数合并。SUMMAXMINREPLACE 都只对自己的列负责。

实验中的 update_time MAX 保留 10:05,status REPLACEpay_amount REPLACE 接受最后一批到达的数据,于是得到:

text 复制代码
9001 | 10:05 | CANCELLED | 0.00

这行数据从未在源系统出现。它不是 Doris 算错,而是表结构允许不同列从不同事件中选值。

Aggregate Key 真正优秀的场景是加法和集合语义稳定的指标,例如:

sql 复制代码
CREATE TABLE ad_counter (
    event_date DATE,
    campaign_id BIGINT,
    impressions BIGINT SUM,
    clicks BIGINT SUM
)
AGGREGATE KEY(event_date, campaign_id)
DISTRIBUTED BY HASH(campaign_id) BUCKETS 8;

广告曝光与点击天然可以按固定维度累加,预聚合能减少查询扫描量。订单当前状态是一个完整业务行,不应该被拆成互不相关的列级聚合。

Unique Key 解决主键覆盖,Sequence 才解决乱序

Unique Key 官方文档 将同 Key 写入解释为 UPSERT。Doris 4.0.8 默认采用 Merge-on-Write:新行写入时识别旧版本,查询直接跳过已经失效的行。

但主键唯一只回答同一个订单保留一行,没有回答哪一行更新。没有 Sequence 时,存储层只能依据写入版本决定胜负,所以迟到的 10:03 仍会覆盖已经可见的 10:05。

function_column.sequence_col = update_time 把业务顺序交给 Doris。同 Key 冲突时,Sequence 更大的版本保留;旧事件即使最后到达,也不能把状态写回过去。更新能力官方文档 同样把 Sequence 定义为乱序数据的版本判定列。

Sequence 也不是万能保险:

  • 业务时间可能重复,秒级时间戳无法区分同一秒内的多次变更;
  • 源库时间可能回拨,不同数据源的时钟未必可比较;
  • 部分更新如果没有携带正确的 Sequence,仍可能产生不符合预期的合并;
  • 删除事件必须进入同一版本体系,不能只同步 Insert 和 Update。

CDC 场景更稳妥的 Sequence 通常是可全序比较的提交位点,例如单源 Binlog 位点、递增业务版本或经过设计的复合版本,而不是随手使用应用服务器时间。

源码真正证明的是失败行会被标记,而不是原地覆盖

Apache Doris 4.0.8 中,MergeIndexDeleteBitmapCalculator 的比较逻辑 会先比较去掉 Sequence 后的主键;主键相同时,Sequence 更大的记录排在前面。随后,重复位置被加入 Delete Bitmap。把源码压缩成不依赖实现细节的伪代码,只剩下面这条链:

text 复制代码
按主键扫描候选行
同 Key 时让较大 Sequence 排在前面
第一行成为当前保留版本
后续同 Key 行记录 Rowset、Segment、RowId
把这些位置加入 Delete Bitmap
查询跳过位图中的行,Compaction 再回收旧版本

跨历史 Rowset 的冲突由 BaseTablet::calc_segment_delete_bitmap 处理。代码路径会查找已经存在的同 Key 行;如果旧 Rowset 中存在更大的 Sequence,新到的当前行反而被加入 Delete Bitmap。反之,旧行被标记失效。

text 复制代码
新 Rowset 的主键
    ↓
查找历史 Rowset 中的同 Key 行
    ↓
历史 Sequence 更大 → 标记新行失效
新 Sequence 更大   → 标记历史行失效
    ↓
提交版本后对查询可见

这就是 Merge-on-Write 的核心差异:它不是修改旧 Segment 中的一行,而是写入新版本,再用 Delete Bitmap 决定查询应该看见谁。代价是写入阶段需要主键查找、冲突计算和位图维护,后台 Compaction 还要回收失效版本。只追加且从不更新的数据使用 Unique Key,只会为不需要的能力付费。

放到 MySQL 和 ClickHouse 中,差异才真正清楚

下面的比较固定同一批乱序 CDC 数据、同一主键和写入后立即查询的条件,只改变冲突处理机制。

方案 冲突何时处理 乱序依据 立即查询的结果 主要代价
MySQL ON DUPLICATE KEY UPDATE 单行事务更新时 默认按语句到达顺序,需自行增加版本条件 主键行立即更新 行锁、索引维护与 OLTP 写放大
ClickHouse ReplacingMergeTree(version) 后台 Merge,或查询使用 FINAL Version 最大值 未合并且不加 FINAL 时可能看见多版本 最终去重或查询时去重成本
Doris Unique Key MoW + Sequence 新版本写入和发布阶段 Sequence 最大值 事务可见后直接得到唯一逻辑行 写入查找、Delete Bitmap 与 Compaction

MySQL 官方文档 中的 ON DUPLICATE KEY UPDATE 本质是主键冲突后执行更新,它不会自动理解业务版本。要防止旧事件覆盖,仍需在更新表达式中增加版本判断。

ClickHouse 官方说明 指出 ReplacingMergeTree 的后台合并是异步的,即使配置 Version,也不能依赖合并时机获得即时去重;需要即时正确性时使用 FINAL。它适合高吞吐不可变写入,而 Doris MoW 把更多去重成本放到写入和发布阶段,换取查询侧直接读取唯一逻辑行。

没有脱离场景的绝对优胜。交易更新与约束仍是 MySQL 的主场;吞吐优先且能接受最终去重时,ReplacingMergeTree 有明显价值;持续 CDC 写入后马上要做点查、聚合和 Join,Doris Unique Key MoW 才体现优势。

建表前只判断数据究竟是哪一种事实

数据本质 正确模型 不要付出的错误代价
不可变事件、日志、审计轨迹 Duplicate Key 不要为无更新需求维护主键去重
可交换、可结合的固定指标 Aggregate Key 不要把完整业务行拆成列级聚合
每个主键只保留当前状态 Unique Key 不要忘记为乱序设计 Sequence

如果业务既要完整历史,又要快速查询当前状态,最稳妥的设计通常是两张表:Duplicate Key 保存事件真相,Unique Key 保存服务状态。前者负责可追溯,后者负责低成本查询,二者通过主键与版本做持续对账。

选错 Key 模型最危险的地方不是查询变慢,而是每条 SQL 都能成功,结果却稳定地表达了错误业务语义。

官方资料与源码

相关推荐
数智启示录26 分钟前
Apache Doris 4.0.8 CDC 正确性(第 9 篇):Flink Checkpoint 一直成功,表里为什么仍是旧数据
大数据·数据库·经验分享·面试·flink
Thomas214328 分钟前
flink处理迟到数据
大数据·flink
CIO401 小时前
AI未来--IT人面试36计
人工智能·面试·职场和发展
拾光Ծ2 小时前
【MySQL】对表数据的操作:增删查改(CRUD)
android·数据库·sql·mysql
IT研究室3 小时前
最新大数据毕业设计选题推荐-基于大数据的电商与本地服务消费评论数据可视化分析-大数据-Spark-Hadoop-Bigdata
大数据·信息可视化·课程设计
大大大大晴天️4 小时前
从 HDFS 到对象存储:计算存储分离如何重塑云原生大数据底座
大数据·云原生
kyriewen13 小时前
我把今年流传的前端 AI 面试题整理了一遍——4 类场景题+回答框架(附速查表)
前端·面试·程序员
大大大大晴天14 小时前
从 HDFS 到对象存储:计算存储分离如何重塑云原生大数据底座
大数据
这个DBA有点耶15 小时前
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
数据库·分布式·dba