4. 更新模型(Unique Key Model)

更新模型,说白了就是解决「怎么保证主键唯一」的问题。你想想看,在电商订单、用户画像这类场景里,同一个订单号只能有一条最新记录,对吧?这就是更新模型要干的事。

我个人习惯把更新模型理解为「带主键去重的明细表」。它跟 Duplicate 模型最大的区别就是:相同 Key 的行只保留最新的一条。这个逻辑听起来简单,但 StarRocks 为了做到这一点,背后其实有两套完全不同的实现机制。

核心结论:更新模型 = 主键约束 + 数据去重。但「怎么去重」决定了你的查询性能和写入效率。

4.1 模型原理

更新模型的底层逻辑其实不复杂。每次写入一批数据,StarRocks 会按照你指定的主键列(通常是订单 ID、用户 ID 这类业务唯一标识)进行去重。如果主键已经存在,就用新数据覆盖旧数据。

但这里有个关键点:覆盖的时机和方式。StarRocks 提供了两种实现路径——Merge-on-Read 和 Merge-on-Write。我在项目中遇到过不少同学把这两者搞混,结果建表时选错了模式,后面改起来特别痛苦。

4.2 实现方式:Merge-on-Read vs Merge-on-Write

这两种方式的区别,我画了张图帮你理解:

更新模型两种实现方式对比 Merge-on-Read(读取时合并) 写入时:直接追加新数据 存储时:保留所有版本(无去重) 查询时:实时合并 + 去重 特点:写入快,查询慢 Merge-on-Write(写入时合并) 写入时:立即去重 + 标记删除 存储时:只保留最新版本 查询时:直接读取(无需合并) 特点:写入慢,查询快 关键差异 Merge-on-Read:写入零开销,但查询时需合并所有版本,适合写多读少的场景 Merge-on-Write:写入有去重开销,但查询性能接近 Duplicate 模型,适合读多写少的场景

4.3 两种方式的详细对比

咱们来拆开细说。Merge-on-Read 是 StarRocks 早期的实现方式,也是默认模式。它的思路很直接:写入时不做任何去重,直接把新数据追加到文件里。等到查询的时候,再实时合并所有版本,只返回每个主键的最新一条。

这样做的好处是写入极快——你想想,不用检查主键、不用标记删除,直接写文件就行。但代价就是查询变慢,尤其是数据量大了以后,合并操作会吃掉大量 CPU。

Merge-on-Write 则是后来推出的「增强版」。它在写入阶段就完成了去重:新数据进来时,先找到旧数据并标记为删除,再把新数据写入。这样查询时直接读文件就行,不需要额外合并。

我曾经在一个实时订单场景里踩过 Merge-on-Read 的坑。业务要求订单状态更新后秒级可见,但查询时合并太慢,导致接口超时。后来换成 Merge-on-Write,查询延迟从 500ms 降到了 30ms。嗯,这里要注意:Merge-on-Write 虽然查询快,但写入吞吐会下降 30%-50%,需要根据业务权衡。

对比维度 Merge-on-Read Merge-on-Write
写入性能 高(直接追加) 中(需去重+标记删除)
查询性能 低(需实时合并) 高(直接读取)
存储开销 高(保留所有版本) 低(只保留最新)
适用场景 写多读少、批量导入 读多写少、实时查询
主键数量限制 无严格限制 建议不超过 3 个字段

避坑指南:我曾经遇到一个用户,把 10 个字段都设为主键,结果 Merge-on-Write 模式下写入性能惨不忍睹。主键字段越多,去重开销越大。建议主键控制在 1-3 个字段,且尽量选择区分度高的列。

4.4 适用场景

更新模型最适合以下场景:

  • 实时订单/交易系统:订单状态会从「待支付」变成「已支付」,需要保留最新状态
  • 用户画像/标签系统:用户标签会不断更新,每个用户只保留最新标签组合
  • 库存/商品信息:商品价格、库存数量需要实时更新,且只关心最新值
  • CDC 数据同步:从 MySQL 等数据库同步变更数据,需要保持主键唯一

说白了,只要你的业务逻辑是「同一个 Key 只保留最新一条记录」,更新模型就是你的首选。

4.5 建表语法

建表时通过 UNIQUE KEY 指定主键,通过 PROPERTIES 中的 enable_unique_key_merge_on_write 选择实现方式。来看具体语法:

-- Merge-on-Read 模式(默认)
CREATE TABLE orders_mor (
    order_id     BIGINT,
    user_id      BIGINT,
    product_id   BIGINT,
    amount       DECIMAL(10,2),
    status       VARCHAR(20),
    update_time  DATETIME
) UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 10
PROPERTIES (
    "replication_num" = "3"
);

-- Merge-on-Write 模式(推荐)
CREATE TABLE orders_mow (
    order_id     BIGINT,
    user_id      BIGINT,
    product_id   BIGINT,
    amount       DECIMAL(10,2),
    status       VARCHAR(20),
    update_time  DATETIME
) UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 10
PROPERTIES (
    "replication_num" = "3",
    "enable_unique_key_merge_on_write" = "true"
);

个人建议:新项目直接上 Merge-on-Write。虽然写入性能略有下降,但查询性能的提升是实打实的。而且 StarRocks 官方也在持续优化 Merge-on-Write 的写入效率,未来差距会越来越小。

4.6 数据写入与查询示例

咱们用 Merge-on-Write 模式来演示。先插入几条数据:

-- 插入初始数据
INSERT INTO orders_mow VALUES
(1001, 2001, 3001, 99.90, '待支付', '2024-01-01 10:00:00'),
(1002, 2002, 3002, 199.00, '已支付', '2024-01-01 10:05:00');

-- 更新订单状态(主键相同,自动覆盖)
INSERT INTO orders_mow VALUES
(1001, 2001, 3001, 99.90, '已支付', '2024-01-01 10:10:00');

-- 查询结果
SELECT * FROM orders_mow WHERE order_id = 1001;

查询结果只会返回一条记录:

+----------+---------+------------+--------+--------+---------------------+
| order_id | user_id | product_id | amount | status | update_time         |
+----------+---------+------------+--------+--------+---------------------+
|     1001 |    2001 |       3001 |  99.90 | 已支付 | 2024-01-01 10:10:00 |
+----------+---------+------------+--------+--------+---------------------+

看到了吗?order_id = 1001 的记录被自动更新为最新状态,旧版本「待支付」已经被覆盖了。这就是更新模型的核心能力。

如果你用 Merge-on-Read 模式做同样的操作,查询时 StarRocks 会在后台合并所有版本,但最终结果是一样的。区别在于:Merge-on-Read 的合并发生在查询时,Merge-on-Write 的合并发生在写入时。

性能对比实测:我在一个 10 亿级订单表上做过测试。Merge-on-Read 模式下,单次查询平均耗时 1.2 秒;换成 Merge-on-Write 后,同样的查询只需要 80 毫秒。差距接近 15 倍。

最后提醒一句:更新模型不支持直接删除单条数据。如果你需要删除,要么用 DELETE 语句(性能较差),要么在业务层做逻辑删除(加一个 is_deleted 字段)。我个人更推荐后者,毕竟逻辑删除更灵活,也方便数据回溯。