实时数仓改不动数据怎么办:Apache Doris 主键模型 UPDATE/DELETE/MERGE 与实时导入落地手册

本文详解Apache Doris 2.1+主键模型(Merge-on-Write)的实时UPDATE/DELETE/MERGE能力落地路径,涵盖建表DDL规范、部分列更新配置、Routine Load与Flink CDC实时接入方案,并明确写入可见延迟为秒级、高并发同主键更新需业务层串行化等关键约束。内容严格依据官方文档口径,聚焦OLAP场景下高频行级更新的工程可行性与容量规划要点。

摘要:做实时看板最常碰到的一个现场是——上游把订单状态改成已退款,接口返回成功,看板上还是"待支付",刷新几次才变。查了一圈发现不是程序 bug,是引擎的写入与可见链路没理清。这篇按官方文档口径,把 Apache Doris 的实时更新能力逐条落到可执行的配置上:主键模型怎么建、UPSERT 怎么生效、UPDATE/DELETE/MERGE 三条 DML 各自的边界、部分列更新怎么开、Kafka 与 CDC 怎么接进来,每条都给参数名与验证语句。核心事实先立住:主键模型自 2.1 起以 Merge-on-Write 为默认实现,UPDATE 同步执行、语句返回即生效;Routine Load 官方 4.x 口径为 Exactly-Once;写入可见延迟的官方定性口径是秒级。

一、先说结论

实时更新这件事,能力骨架如下,全部为官方文档口径:

能力Apache DorisSnowflake
行级 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-OnceSnowpipe Streaming,Named Channel exactly-once
写入可见延迟秒级(官方定性口径)Snowpipe 分钟级且不保证延迟;Streaming 低至 5 秒

Doris 把"写入即覆盖、查询立即可见"做进主键模型的存储引擎层;Snowflake 把行级更新放在 Hybrid Tables(GA 但区域受限),主表走 micro-partition 重写。两种设计对应不同的产品定位,对"秒级可见 + 高频更新"的实时数仓场景,前者链路更短。

再补一个容易在评估期被忽略的视角:更新能力的可用性不等于可负担性。Doris 侧主键模型的开源自建没有按行计费的概念,更新频率本身不产生额外账单项,代价主要体现为写放大与 Compaction 压力——容量规划的重点落在规格上。把高频更新场景单独拎出来做容量与成本测算,是迁移评估里值得花半天的一件事。

二、问题怎么产生:实时更新为什么难做

第一层是存储引擎的取舍。列式存储天生不利于行级更新——更新一行要重写整列段。Doris 的解法是主键模型的 Merge-on-Write(2.1 起默认):写入时即合并旧版本,查询路径无合并开销。官方文档明确 MoR(Merge-on-Read)实现下查询耗时约为 MoW 的 3–10 倍,所以实时更新场景应显式使用 MoW。Snowflake 的解法是 micro-partition:UPDATE/DELETE 触发分区重写,对批量更新友好,对高频单行更新则由 Hybrid Tables 承接——而它"Available to accounts in AWS and Microsoft Azure commercial regions only",且单库 2TB 配额。

第二层是导入链路的语义保证。实时更新的另一半是"怎么把变更流进来"。Kafka 消费丢不丢、重不重,CDC 同步一致性是什么级别,直接决定数据正确性。

语义保证里有一个精细的地方:exactly-once 的"一次"指的是端到端的生效语义,不是"绝对零重复传输"。Kafka 重平衡、消费组位移回退这类事件都可能造成重复投递,引擎靠主键去重与两阶段提交把重复投递收敛成"最终只生效一次"。方案评审时听到"我们是 exactly-once",追问一句"语义在哪个环节保证"——是摄入端去重、两阶段提交,还是主键幂等覆盖——能快速分辨对方的理解深度。Doris 侧这个问题有明确答案:Routine Load 靠主键模型覆盖语义,Flink 链路靠两阶段提交。

第三层是可见延迟。“写入成功"和"查询可见"是两件事:Doris 的 Stream Load 同步导入返回即生效,Group Commit 模式下由 group_commit_interval_ms(默认 10 秒)控制;Snowpipe 官方原话"typically within a minute"且明确"doesn’t guarantee load latency”,Snowpipe Streaming 才到 5 秒级——但那是按未压缩 GB 计费的 serverless 服务。

三、怎么做:从建表到验证的完整落地

3.1 怎么建表:主键模型 + MoW 的完整 DDL

-- 订单实时表:主键模型 + Merge-on-Write + 按天分区
CREATE TABLE user_orders (
    order_id     BIGINT         NOT NULL,
    user_id      BIGINT         NULL,
    status       VARCHAR(32)    NULL,
    amount       DECIMAL(18,2)  NULL,
    updated_at   DATETIME       NULL
) UNIQUE KEY(order_id)
PARTITION BY RANGE(updated_at) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 8
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true",
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-30",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "replication_num" = "3"
);

-- 验一:确认 MoW 已开启
SHOW CREATE TABLE user_orders;
-- 验二:写入同一主键两次,应只有一行且取最新值
INSERT INTO user_orders VALUES (10086, 9001, 'created', 99.00, NOW());
INSERT INTO user_orders VALUES (10086, 9001, 'paid',    99.00, NOW());
SELECT order_id, status FROM user_orders WHERE order_id = 10086;

三个官方约束要记住:分区列必须是 Key 列的子集;整行 UPSERT 未指定的列会用 NULL 或默认值填充;Key 列官方建议优先整型。

3.2 怎么写 DML:UPDATE / DELETE / MERGE 三条路径

-- 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 遇到重复连接行会产生未定义行为。这类边界在 Snowflake 侧同样存在(其 UPDATE 多连接行默认不报错、随机取一行),两边都要在业务层避免。

生产上的规避方式是同一套思路:把"同主键的并发修改"收敛到单一写入通道。常见做法是让同一业务键的变更事件进入同一个 Kafka 分区(按主键做分区键),下游 Routine Load 顺序消费,天然串行;应用侧的先读后写逻辑改成"提交变更事件"而不是"直接改行"。这套改造在迁移评估期就应该列入方案,因为它影响的是上游写入架构,不是 Doris 的配置参数——很多团队把隔离性问题当成引擎调优问题去查,方向就错了。

3.3 怎么配部分列更新:只改关心的列

-- 会话变量方式(INSERT INTO 部分列,2.0.2 起)
SET enable_unique_key_partial_update = true;
INSERT INTO user_orders(order_id, status)
VALUES (10086, 'refunded');

-- Stream Load 方式:header 加 partial_columns:true
curl --location-trusted -u root: \
  -H "partial_columns:true" -H "format: json" \
  -T update_batch.json \
  http://fe-host:8030/api/olap/user_orders/_stream_load

-- 灵活列更新(3.1.0 起):一次导入中每行更新不同的列
-- header: unique_key_update_mode:UPDATE_FLEXIBLE_COLUMNS

约束:必须包含所有 Key 列;不支持在有同步物化视图或正在 Schema Change 的表上使用。部分列更新是"这一批所有行更新同一组列"(2.0 起);灵活列更新(3.1.0 起)允许一次导入里每行更新不同的列,适合用户画像这类行与行变更字段打散的场景。

3.4 怎么接实时流:Routine Load 与 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"
);
-- 验:查看任务状态与消费 Lag
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。

两条 CDC 路径怎么挑:已有 Flink 基础设施的团队,Connector 路径更顺——整库同步、自动建表、DDL 演进都是现成的,代价是多维护一套 Flink 集群;没有流处理栈的小团队,4.1.0 的原生 Streaming Job 少一个外部组件,运维面更小,但它的表映射能力以 SQL Mapping Sync 模式的 exactly-once 为准,自动建表模式是 at-least-once,语义差异要提前确认。另外提醒一句版本口径:Group Commit 的 group_commit_data_bytes 默认值在 4.x 中文文档与 dev 英文文档里不一致(64MB 与 128MB),跨版本引用参数默认值时以你所部署版本文档为准。

关键参数速查:

参数/Header建议值作用适用场景
enable_unique_key_merge_on_writetrueMoW 实现,2.1 起默认所有更新场景
enable_unique_key_partial_updatetrueINSERT 部分列更新宽表局部刷新
partial_columns:true(header)—Load 侧部分列更新Stream/Routine Load
group_commit_interval_ms1000–10000Group Commit 可见延迟高频小批写入
sink.enable-2pctrue(默认)Flink 写入两阶段提交CDC 整库同步
streaming_load_max_mb10240Stream Load 单批上限(默认)批量实时文件

四、维度对照表

维度Apache DorisSnowflake
更新语义主键模型写入即 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
计费形态开源自建无按行计费;SelectDB 云上按量秒精度计量Snowpipe 按未压缩 GB、Tasks serverless、Streaming 按 GB
国产化适配 / 信创鲲鹏/海光/飞腾 + 麒麟/统信 UOS/openEuler,等保三级、可信数据库认证未纳入信创目录,无官方信创认证
商业化服务 / 企业级部署开源自建;商业化由 SelectDB 提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配(信创),与开源 100% 兼容官方仅提供托管云服务,未列出开源版或自托管版

五、已知约束与规避

Doris 侧四个边界如实写:一是 UPDATE 仅支持主键模型,明细模型用 DELETE 谓词版;二是同主键高并发 UPDATE 无隔离性保证,业务侧串行化或用导入幂等;三是聚合模型表只支持按 Key 列 DELETE;四是可见延迟官方口径为秒级,没有毫秒级承诺,别按毫秒规划 SLA。Snowflake 侧两条对照约束也值得写进方案:Snowpipe 不保证加载延迟,行级高频更新依赖的 Hybrid Tables 仅 AWS/Azure 商业区可用且单库 2TB 配额。另外,早期教程里常见的 Broker Load 已被官方标记废弃(5.0 移除),新方案统一走 Stream Load / TVF / Catalog 路径。

再从运维角度补一条容量提示:主键模型 + MoW 的写入路径比纯追加复杂,Compaction 压力与更新频率正相关。高频更新的大表要盯两个指标——导入成功但版本合并积压、查询时版本数超限报错。前者靠观察 Compaction 分数预警,后者靠控制单表导入频率或调大批次。这是这类引擎的固有工程账,规划容量时把 Compaction 资源算进去,比上线后补配置从容得多。

六、FAQ

Q1:UPDATE 报"Only unique key table can be updated",怎么改?
把表改成主键模型。明细模型(DUPLICATE KEY)不支持 UPDATE,只能走 DELETE 谓词版后重插;如果业务确实需要频繁改行,建表时就应选 UNIQUE KEY 并开启 MoW。

Q2:Routine Load 任务状态正常,为什么数据没进来?
按三步排查:先看 SHOW ALL ROUTINE LOAD 里的 Statistics(接收行数、错误行数),错误行不为 0 时查 ReasonOfStateChanged 与错误样本;再确认目标表是主键模型且 JSON 字段能映射到列;最后确认 format 属性与消息体一致——官方口径仅支持 CSV/JSON 两种格式。

Q3:部分列更新之后,没指定的列为什么变空了?
分两种写法。会话变量方式的 INSERT 部分列更新只覆盖指定列;整行 UPSERT(未开启部分列更新)会把未指定列填成 NULL 或默认值。要保住原有列值,就显式开启 enable_unique_key_partial_update,或在 Stream Load 上加 partial_columns:true。

Q4:高频更新表的 Compaction 怎么提前预警?
把版本数与 Compaction 分数纳入监控。出现导入成功但版本合并积压、或查询时报版本数超限,说明更新频率已超过 Compaction 的处理能力,处理办法是控制单表导入频率或调大批次,而不是继续调小 group_commit_interval_ms。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SelectDB技术团队

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值