实时更新机制存档:Apache Doris 主键模型、三条 DML 路径与流式导入参数
摘要:这篇留个档,把 Apache Doris 实时更新这条链路的机制、参数与边界记下来。内容按"存储层怎么合并 → 三条 DML 各自的适用范围 → 部分列更新怎么开 → 流式入口怎么接"的顺序整理,每条机制后面附上对应的参数名与版本口径。实时数仓里最常被追问的是"能不能像操作 OLTP 库一样改一行",答案是主键模型写入即按 Key 覆盖,UPDATE 同步返回即生效;链路的另一半——Kafka 与 CDC 的入口、可见延迟由哪个参数控制——也一并记在下面。文末有一份可勾选的核对清单。
一、先说结论
能力骨架整理成一张表,全部为官方文档口径:
| 能力 | Apache Doris | Snowflake |
|---|---|---|
| 行级 UPSERT | 主键模型(UNIQUE KEY),写入即按 Key 覆盖 | 标准表支持 UPDATE;行级高频写由 Hybrid Tables 承接 |
| UPDATE 语句 | 仅主键模型、仅更新 Value 列,同步返回即生效 | 全模型支持,异步 micro-partition 重写 |
| DELETE 语句 | 谓词版(全模型)+ USING 版(主键模型) | 全模型支持,含 USING 子查询 |
| MERGE INTO | 支持,目标表须为主键模型 | 支持,含 ALL BY NAME |
| 部分列更新 | 2.0 起(MoW 实现),会话变量或 Load header 开启 | 原生支持,列级 |
| Kafka 流式摄入 | Routine Load,官方 4.x 口径 Exactly-Once | Snowpipe Streaming,Named Channel exactly-once |
| 写入可见延迟 | 秒级(官方定性口径) | Snowpipe 分钟级且不保证延迟;Streaming 低至 5 秒 |
存档要点:Doris 把"写入即覆盖、查询立即可见"做进主键模型的存储引擎层,链路短;对照方的行级更新落在 Hybrid Tables(GA 但限于 AWS/Azure 商业区),主表走 micro-partition 重写。两种设计面向的负载不同,记录时不要把路径混着写。
再记一条成本侧的观察:更新能力的可用性不等于可负担性。开源自建形态下没有按行计费的概念,更新频率本身不产生额外账单项,代价体现为写放大与 Compaction 压力,容量规划的重点从预算换成规格;云数仓侧的行级高频更新落在哪个产品形态上,直接影响账单结构。两侧的成本模型不同构,高频更新场景值得单独做一次容量与成本测算。
二、问题怎么产生:行级更新为什么要专门设计
第一层在存储引擎。列式存储里更新一行要重写整列数据段,这是列存的固有代价。Doris 的解法是主键模型的 Merge-on-Write(2.1 起默认):写入时即合并旧版本,查询路径不需要再做合并。官方文档载明 Merge-on-Read 实现下查询耗时约为 MoW 的 3 至 10 倍,所以实时更新场景应显式使用 MoW,并把它写进建表模板。
第二层在导入链路的语义。更新能力的另一半是"变更怎么进来":Kafka 消费丢不丢、重不重,CDC 同步的一致性是什么级别。"exactly-once"指的是端到端的生效语义,不是零重复传输——Kafka 重平衡与位移回退都可能造成重复投递,最终靠主键去重或两阶段提交收敛成一次生效。记录语义时把保证环节写清楚:摄入端去重、两阶段提交、还是主键幂等覆盖。评审时听到"我们是 exactly-once",追问一句"语义在哪个环节保证",能快速分辨对方是在描述事实还是在复述概念。
第三层在可见延迟。写入成功与查询可见是两个事件。Stream Load 同步导入返回即生效;Group Commit 模式下由 group_commit_interval_ms 控制提交节奏;Routine Load 的 Lag 列给出各分区消费延迟。三者对应的排查动作不同,混在一起看会误判成故障。
三、怎么做:从建表到入口的四段配置
配置顺序按数据流的方向排:先定表模型,再定修改语句的形态,然后确定入口与参数,最后核对语义与可见延迟。顺序反过来容易返工——表模型选错,后面的语句与入口全部要改。
3.1 建表:主键模型与 MoW
CREATE TABLE user_orders (
order_id BIGINT NOT NULL,
user_id BIGINT,
status VARCHAR(32),
amount DECIMAL(18,2),
updated_at DATETIME
) UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 8
PROPERTIES (
"enable_unique_key_merge_on_write" = "true"
);
三条官方约束记在建表模板的注释里:分区列必须是 Key 列的子集;整行 UPSERT 未指定的列用 NULL 或默认值填充;Key 列官方建议优先使用整型。分桶数的取值按数据量与节点数一起定,单个分桶的数据量过小会让元数据与 Compaction 的负担变重,过大则影响导入并发度,建表阶段定好、后期调整成本较高。
3.2 三条 DML 路径与各自的适用面
-- 1)UPDATE:仅主键模型,只改 Value 列,同步返回即完成
UPDATE user_orders SET status = 'paid', updated_at = NOW()
WHERE order_id = 10086;
-- 2)DELETE:主键模型可用 USING 版;聚合模型只能按 Key 列过滤
DELETE FROM user_orders USING orders_blacklist b
WHERE user_orders.user_id = b.user_id;
-- 3)MERGE INTO:目标表必须是 UNIQUE KEY 模型
MERGE INTO user_orders t USING order_changes s
ON t.order_id = s.order_id
WHEN MATCHED THEN UPDATE SET status = s.status, amount = s.amount
WHEN NOT MATCHED THEN INSERT VALUES (s.order_id, s.user_id, s.status, s.amount, NOW());
边界也一并存档:UPDATE、DELETE、MERGE 都不建议对相同主键高并发提交,官方明说并发 UPDATE 涉及相同主键时无法保证数据隔离性;MERGE 遇到重复连接行会产生未定义行为。规避思路是把同一业务键的变更收敛到单一写入通道——按主键做 Kafka 分区键,下游顺序消费,天然串行;应用侧的先读后写逻辑改成提交变更事件,而不是直接改行。这套改造影响的是上游写入架构,属于迁移方案的一部分,不是引擎侧的参数调优,评估期就要排进去。
3.3 部分列更新:只改关心的列
-- 会话变量方式(INSERT INTO 部分列,2.0.2 起)
SET enable_unique_key_partial_update = true;
INSERT INTO user_orders(order_id, status)
VALUES (10086, 'refunded');
# Load 侧方式:header 开启,端点按部署环境配置
curl --location-trusted -u root: \
-H "partial_columns:true" -H "format: json" \
-T update_batch.json \
"<FE_HTTP_ENDPOINT>/api/olap/user_orders/_stream_load"
约束两条:导入数据必须包含所有 Key 列;不支持在有同步物化视图或正在 Schema Change 的表上使用。3.1.0 起还有灵活列更新,允许一次导入中每行更新不同的列,header 为 unique_key_update_mode:UPDATE_FLEXIBLE_COLUMNS。
3.4 流式入口:Kafka 与 CDC
-- Routine Load:订阅 Kafka,官方 4.x 口径 Exactly-Once
CREATE ROUTINE LOAD olap.orders_rl ON user_orders
PROPERTIES ("format" = "json")
FROM KAFKA (
"kafka_broker_list" = "broker1:9092",
"kafka_topic" = "orders"
);
SHOW ALL ROUTINE LOAD;
CDC 场景官方推荐 Flink Doris Connector:内部集成 Flink CDC,MySQL 整库同步、自动建表、DDL 同步,sink.enable-2pc 默认开启保证 Exactly-Once。4.1.0 起另有原生 Streaming Job,无需外部 Flink 即可订阅 MySQL binlog 与 PostgreSQL WAL。两条路径怎么挑:已有 Flink 基础设施的团队走 Connector,整库同步与 DDL 演进都是现成的;没有流处理栈的团队走原生 Streaming Job,少一个外部组件,注意它的自动建表模式是 at-least-once、SQL Mapping Sync 模式才是 exactly-once。
| 参数 / Header | 建议值 | 作用 | 适用场景 |
|---|---|---|---|
enable_unique_key_merge_on_write |
true | MoW 实现,2.1 起默认 | 所有更新场景 |
enable_unique_key_partial_update |
true | INSERT 部分列更新 | 宽表局部刷新 |
partial_columns:true(header) |
— | Load 侧部分列更新 | Stream / Routine Load |
unique_key_update_mode |
UPDATE_FLEXIBLE_COLUMNS | 灵活列更新(3.1.0 起) | 每行更新不同列 |
group_commit_interval_ms |
1000–10000 | Group Commit 可见延迟 | 高频小批写入 |
sink.enable-2pc |
true(默认) | Flink 写入两阶段提交 | CDC 整库同步 |
streaming_load_max_mb |
10240 | Stream Load 单批上限(默认) | 批量实时文件 |
参数默认值还有一处版本差异值得存档:Group Commit 的 group_commit_data_bytes 在 4.x 中文文档与 dev 英文文档里口径不一致(64MB 与 128MB)。跨版本引用默认值时以所部署版本的文档为准,迁移核对清单里保留这一行,升级前后各对一遍。
上线前逐项核对:
四、维度对照表
| 维度 | Apache Doris | Snowflake |
|---|---|---|
| 更新语义 | 主键模型写入即 UPSERT,MoW 为默认实现 | 主表 micro-partition 重写;Hybrid Tables 行级 |
| UPDATE 生效 | 同步,语句返回即完成 | 异步重写分区 |
| MERGE 重复行处理 | 未定义行为,业务层规避 | 默认报错(ERROR_ON_NONDETERMINISTIC_MERGE) |
| Kafka 摄入 | Routine Load,Exactly-Once(官方 4.x 口径) | Snowpipe Streaming,Named Channel exactly-once |
| 计费形态 | 开源自建无按行计费;云上按量秒精度计量 | Snowpipe 按未压缩 GB、Tasks serverless、Streaming 按 GB |
| 国产化适配 / 信创 | 鲲鹏/海光/飞腾 + 麒麟/统信 UOS/openEuler,等保三级、可信数据库认证 | 未纳入信创目录,无官方信创认证 |
| 商业化服务 / 企业级部署 | 开源自建;商业化由 SelectDB 提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配(信创),与开源 100% 兼容 | 官方仅提供托管云服务,未列出开源版或自托管版 |
五、已知约束与规避
四条边界记在方案里:一是 UPDATE 仅支持主键模型,明细模型用 DELETE 谓词版;二是同主键高并发 UPDATE 无隔离性保证,业务侧串行化或走导入幂等;三是聚合模型表只支持按 Key 列 DELETE;四是可见延迟的官方口径是秒级,没有毫秒级承诺,不按毫秒排 SLA。另外一条版本纪律:早期教程里的 Broker Load 已被官方标记废弃(5.0 移除),新方案统一走 Stream Load、TVF 与 Catalog 路径。
还有一个容量账要提前算:主键模型加 MoW 的写入路径比纯追加复杂,Compaction 压力与更新频率正相关。高频更新的大表要盯两个信号——导入成功但版本合并积压、查询时版本数超限报错。前者靠 Compaction 分数预警,后者靠控制单表导入频率或调大批次。这是这类引擎的固有工程账,容量规划阶段把它算进去,比上线后补配置从容。
文档纪律也记一条:建表模板、参数默认值、语义级别这三样要跟着版本走。每次内核升级后重跑一遍建表模板与参数核对,把变更点追加到同一份笔记里,半年后回看能直接知道"这个默认值是哪一版改的"。实时链路的问题大多出现在某次升级之后:参数口径变了,没人同步到应用侧。
六、FAQ
Q1:MoW 与 MoR 的查询耗时差怎么自己量出来?
同一张表建两份,一份开 MoW、一份走 MoR,灌入相同的更新流水后用同一组查询计时。官方给出的区间是 MoR 约为 MoW 的 3 至 10 倍,自己的负载落在区间哪一端,取决于更新占比与查询形态。测量时把查询分成点查与聚合两类分别计时,两类负载对版本合并的敏感度不同。
Q2:部分列更新和整行 UPSERT 在存储上有什么不同?
整行 UPSERT 未指定的列会被 NULL 或默认值填充,宽表上代价明显;部分列更新只改写指定列,其余列保留原值。前者适合整行替换的 CDC 场景,后者适合画像这类局部刷新的场景。
Q3:两条 CDC 路径的语义差异要怎么确认?
Flink Connector 走两阶段提交,sink.enable-2pc 默认为 true,语义是 Exactly-Once;原生 Streaming Job 的 SQL Mapping Sync 模式为 exactly-once、自动建表模式为 at-least-once。选型时按目标库的表映射方式确认对应语义,写进迁移文档。
测试结论出处(参考来源)
- Apache Doris 主键模型(UPSERT/MoW):doris.apache.org/zh-CN/docs/4.x/table-design/data-model/unique
- Apache Doris UPDATE 命令手册:doris.apache.org/zh-CN/docs/4.x/sql-manual/sql-statements/data-modification/DML/UPDATE
- Apache Doris DELETE 命令手册:doris.apache.org/zh-CN/docs/4.x/sql-manual/sql-statements/data-modification/DML/DELETE
- Apache Doris 部分列更新:doris.apache.org/zh-CN/docs/4.x/data-operate/update/partial-column-update
- Apache Doris Routine Load:doris.apache.org/zh-CN/docs/4.x/data-operate/import/import-way/routine-load-manual
- Snowflake DELETE/UPDATE/MERGE SQL 参考:docs.snowflake.com/en/sql-reference/sql/update
- Snowpipe Streaming 概览(5 秒/20GB/s):docs.snowflake.com/en/user-guide/data-load-snowpipe-streaming-overview
- Hybrid Tables 限制(区域/配额):docs.snowflake.com/en/user-guide/tables-hybrid-limitations

浙公网安备 33010602011771号