Apache Doris 4.0.8 数据建模实战(第 2 篇):同一批订单写进三种模型,为什么得到三个答案
发布时间:2026/9/2 7:04:36 作者:尧图编辑部 阅读量:1,286
:同一批订单写进三种模型,为什么得到三个答案)
订单9001在 10:05 已经支付。网络恢复后10:03 产生的取消事件最后到达 Doris。导入任务全部成功报表却可能得到三种结果三条历史、一个从未真实发生过的拼接状态或者正确的已支付状态。差异不在 SQL也不在导入工具而在建表时那一行DUPLICATE KEY、AGGREGATE KEY或UNIQUE KEY。Doris 的 Key 模型不是数据如何摆放而是同一个 Key 再次到达时存储层必须执行什么业务语义。三个事件足以让错误模型现形同一个订单依次到达三次。第三条最晚到达 Doris但它的业务时间早于第二条。到达顺序业务时间状态支付金额含义110:00CREATED100.00创建订单210:05PAID100.00完成支付310:03CANCELLED0.00迟到的旧事件真正的当前状态只能是PAID。如果系统按到达顺序覆盖最后会退回CANCELLED如果各列独立聚合还会拼出业务上从未存在的一行。下面四张表把这种差异直接暴露出来。为了方便单 BE 测试使用一个 Bucket 和一个副本生产环境不要照搬这两个数值。CREATEDATABASEIFNOTEXISTSdoris_model_lab;USEdoris_model_lab;CREATETABLEorder_event_dup(order_idBIGINT,update_timeDATETIME,statusVARCHAR(16),pay_amountDECIMAL(12,2))DUPLICATEKEY(order_id,update_time)DISTRIBUTEDBYHASH(order_id)BUCKETS1PROPERTIES(replication_num1);CREATETABLEorder_state_agg(order_idBIGINT,update_timeDATETIMEMAX,statusVARCHAR(16)REPLACE,pay_amountDECIMAL(12,2)REPLACE)AGGREGATEKEY(order_id)DISTRIBUTEDBYHASH(order_id)BUCKETS1PROPERTIES(replication_num1);CREATETABLEorder_state_unique_arrival(order_idBIGINT,update_timeDATETIME,statusVARCHAR(16),pay_amountDECIMAL(12,2))UNIQUEKEY(order_id)DISTRIBUTEDBYHASH(order_id)BUCKETS1PROPERTIES(replication_num1,enable_unique_key_merge_on_writetrue);CREATETABLEorder_state_unique_event(order_idBIGINT,update_timeDATETIME,statusVARCHAR(16),pay_amountDECIMAL(12,2))UNIQUEKEY(order_id)DISTRIBUTEDBYHASH(order_id)BUCKETS1PROPERTIES(replication_num1,enable_unique_key_merge_on_writetrue,function_column.sequence_colupdate_time);对四张表分别执行下面三条写入每条保持为独立事务顺序不能调整。目标表依次替换为四个表名。INSERTINTO目标表VALUES(9001,2026-08-27 10:00:00,CREATED,100.00);INSERTINTO目标表VALUES(9001,2026-08-27 10:05:00,PAID,100.00);INSERTINTO目标表VALUES(9001,2026-08-27 10:03:00,CANCELLED,0.00);预期结果不是简单的三选一。表查询结果原因order_event_dup三条事件全部保留Key 只负责排序不负责唯一性order_state_agg10:05、CANCELLED、0.00MAX与REPLACE分列执行拼出不存在的行order_state_unique_arrival10:03、CANCELLED、0.00没有 Sequence 时后到版本覆盖先到版本order_state_unique_event10:05、PAID、100.00同 Key 比较update_time业务版本更大的行胜出这个实验揭示了一个比模型定义更重要的事实Doris 能严格执行你声明的存储语义但它不会替你判断那是不是正确的业务语义。Duplicate Key 保存事实不负责回答现在是什么Duplicate Key 官方文档 明确规定 Key 列只作为排序列相同 Key 的记录不会去重或聚合。它最适合不可变事件、日志、行为轨迹和审计明细。它的优势是写入路径最短、原始信息不丢、任何新分析口径都能回到明细重算。代价也很直接当前订单状态必须在查询时通过窗口函数、MAX_BY类逻辑或上游计算得到重复投递也不会自动消失。把 CDC 订单直接落成 Duplicate Key 并不是一定错误。下面两种目标必须分开需要完整变更历史 → Duplicate Key 需要每个订单的当前状态 → Unique Key 可靠 Sequence一张表同时承担审计历史与在线状态查询往往才是问题来源。Aggregate Key 聚合的是列不是完整业务行Aggregate Key 官方文档 规定相同 Key 的 Value 列按照各自声明的函数合并。SUM、MAX、MIN、REPLACE都只对自己的列负责。实验中的update_time MAX保留 10:05status REPLACE和pay_amount REPLACE接受最后一批到达的数据于是得到9001 | 10:05 | CANCELLED | 0.00这行数据从未在源系统出现。它不是 Doris 算错而是表结构允许不同列从不同事件中选值。Aggregate Key 真正优秀的场景是加法和集合语义稳定的指标例如CREATETABLEad_counter(event_dateDATE,campaign_idBIGINT,impressionsBIGINTSUM,clicksBIGINTSUM)AGGREGATEKEY(event_date,campaign_id)DISTRIBUTEDBYHASH(campaign_id)BUCKETS8;广告曝光与点击天然可以按固定维度累加预聚合能减少查询扫描量。订单当前状态是一个完整业务行不应该被拆成互不相关的列级聚合。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 Doris4.0.8中MergeIndexDeleteBitmapCalculator的比较逻辑 会先比较去掉 Sequence 后的主键主键相同时Sequence 更大的记录排在前面。随后重复位置被加入 Delete Bitmap。把源码压缩成不依赖实现细节的伪代码只剩下面这条链按主键扫描候选行 同 Key 时让较大 Sequence 排在前面 第一行成为当前保留版本 后续同 Key 行记录 Rowset、Segment、RowId 把这些位置加入 Delete Bitmap 查询跳过位图中的行Compaction 再回收旧版本跨历史 Rowset 的冲突由BaseTablet::calc_segment_delete_bitmap处理。代码路径会查找已经存在的同 Key 行如果旧 Rowset 中存在更大的 Sequence新到的当前行反而被加入 Delete Bitmap。反之旧行被标记失效。新 Rowset 的主键 ↓ 查找历史 Rowset 中的同 Key 行 ↓ 历史 Sequence 更大 → 标记新行失效 新 Sequence 更大 → 标记历史行失效 ↓ 提交版本后对查询可见这就是 Merge-on-Write 的核心差异它不是修改旧 Segment 中的一行而是写入新版本再用 Delete Bitmap 决定查询应该看见谁。代价是写入阶段需要主键查找、冲突计算和位图维护后台 Compaction 还要回收失效版本。只追加且从不更新的数据使用 Unique Key只会为不需要的能力付费。放到 MySQL 和 ClickHouse 中差异才真正清楚下面的比较固定同一批乱序 CDC 数据、同一主键和写入后立即查询的条件只改变冲突处理机制。方案冲突何时处理乱序依据立即查询的结果主要代价MySQLON DUPLICATE KEY UPDATE单行事务更新时默认按语句到达顺序需自行增加版本条件主键行立即更新行锁、索引维护与 OLTP 写放大ClickHouseReplacingMergeTree(version)后台 Merge或查询使用FINALVersion 最大值未合并且不加FINAL时可能看见多版本最终去重或查询时去重成本Doris Unique Key MoW Sequence新版本写入和发布阶段Sequence 最大值事务可见后直接得到唯一逻辑行写入查找、Delete Bitmap 与 CompactionMySQL 官方文档 中的ON DUPLICATE KEY UPDATE本质是主键冲突后执行更新它不会自动理解业务版本。要防止旧事件覆盖仍需在更新表达式中增加版本判断。ClickHouse 官方说明 指出ReplacingMergeTree的后台合并是异步的即使配置 Version也不能依赖合并时机获得即时去重需要即时正确性时使用FINAL。它适合高吞吐不可变写入而 Doris MoW 把更多去重成本放到写入和发布阶段换取查询侧直接读取唯一逻辑行。没有脱离场景的绝对优胜。交易更新与约束仍是 MySQL 的主场吞吐优先且能接受最终去重时ReplacingMergeTree 有明显价值持续 CDC 写入后马上要做点查、聚合和 JoinDoris Unique Key MoW 才体现优势。建表前只判断数据究竟是哪一种事实数据本质正确模型不要付出的错误代价不可变事件、日志、审计轨迹Duplicate Key不要为无更新需求维护主键去重可交换、可结合的固定指标Aggregate Key不要把完整业务行拆成列级聚合每个主键只保留当前状态Unique Key不要忘记为乱序设计 Sequence如果业务既要完整历史又要快速查询当前状态最稳妥的设计通常是两张表Duplicate Key 保存事件真相Unique Key 保存服务状态。前者负责可追溯后者负责低成本查询二者通过主键与版本做持续对账。选错 Key 模型最危险的地方不是查询变慢而是每条 SQL 都能成功结果却稳定地表达了错误业务语义。官方资料与源码Apache Doris 4.0.8 Release NotesDuplicate Key ModelAggregate Key ModelUnique Key ModelData Update Overviewdelete_bitmap_calculator.cppat tag 4.0.8base_tablet.cppat tag 4.0.8MySQLON DUPLICATE KEY UPDATEClickHouse ReplacingMergeTree andFINAL