同一个 Key 写入两次,StarRocks 会留下什么?四种表模型讲明白了

上一篇讲完分区和分桶后,我们知道了一行数据会怎样分布到不同 BE。

但真正建表时,还有一个绕不开的问题:

同一笔业务数据后来发生了变化,新数据再次写入时,旧数据应该保留、替换,还是与新数据合并?

这个选择,正是 StarRocks 四种表模型要解决的问题。

如果你还没看过上一篇,可以先从这里开始:

👉《数据为什么会分到不同 BE?StarRocks 的分区和分桶终于讲清了》

StarRocks 当前提供四种表模型:

  • 明细模型(Duplicate Key);

  • 主键模型(Primary Key);

  • 聚合模型(Aggregate Key);

  • 更新模型(Unique Key)。

这篇不先背定义。我们从两次写入开始,看它们分别会留下什么。

一笔订单,状态发生了变化

假设订单 10001 在 10:00 创建,当时还是"待支付":

ini 复制代码
order_id = 10001
status   = 待支付
amount   = 100

五分钟后,用户完成支付,系统又同步来一条数据:

ini 复制代码
order_id = 10001
status   = 已支付
amount   = 100

两条数据的 order_id 相同,但状态不同。

StarRocks 应该怎样处理?

没有唯一答案。

如果我们要分析订单状态变化过程,两条都应该留下;如果只关心订单现在是什么状态,就只应该看到"已支付"。如果保存的不是订单明细,而是每天的销售额小计,相同日期和城市的数据又应该相加。

表模型决定的,就是相同 Key 数据写入后怎样处理。

图 1:明细模型保留每次写入;主键和更新模型返回最新值;聚合模型按照指定函数合并指标。

先用四句话建立整体印象:

js 复制代码
明细模型:都留下
主键模型:保留最新状态
聚合模型:按规则合并
更新模型:同样返回最新状态,但属于较早的实现

下面逐个来看。

明细模型:每次写入都保留

明细模型对应 DUPLICATE KEY。

这里的 Duplicate 就是"允许重复"。即使两行数据的 Key 相同,新数据也不会自动替换旧数据。

订单 10001 连续写入两次后,表中可以同时存在:

apache 复制代码
10001  待支付  100  10:00
10001  已支付  100  10:05

所以明细模型适合保存:

  • 日志;

  • 用户行为事件;

  • 操作记录;

  • 只追加、不修改的原始数据;

  • 需要保留每次变化历史的流水数据。

它的特点不是"不能出现重复",恰恰是相同 Key 的数据可以继续存在。

一份简化建表语句可以写成:

java 复制代码
CREATE TABLE order_events (
    order_id   BIGINT,
    status     VARCHAR(20),
    amount     DECIMAL(18, 2),
    event_time DATETIME
)
DUPLICATE KEY(order_id, event_time)
PARTITION BY date_trunc('month', event_time)
DISTRIBUTED BY HASH(order_id);

这里的 DUPLICATE KEY 不提供唯一性约束。它不能像 MySQL 主键那样保证 order_id 只出现一次。

如果业务想直接查询"每笔订单当前的最新状态",却把每次变化都写进明细模型,那么查询时还要自己从多条记录中找出最新一条。

因此,要历史过程,用明细模型;只要当前状态,继续看主键模型。

主键模型:相同主键只保留最新状态

主键模型对应 PRIMARY KEY。

主键用于唯一标识一行数据,并且具有唯一、非空约束。当相同主键的新数据写入时,新数据会替代旧数据。

仍然写入前面的两条订单数据,查询时看到的是:

apache 复制代码
10001  已支付  100

旧的"待支付"状态不会继续作为当前结果返回。

这类模型适合保存:

  • 订单当前状态;

  • 用户当前资料;

  • 商品当前库存;

  • 从 MySQL 等业务数据库同步过来的最新数据;

  • 需要更新、删除或部分列更新的数据。

简化建表语句如下:

java 复制代码
CREATE TABLE orders (
    order_id   BIGINT,
    status     VARCHAR(20),
    amount     DECIMAL(18, 2),
    update_time DATETIME
)
PRIMARY KEY(order_id)
DISTRIBUTED BY HASH(order_id);

如果 MySQL 中的订单状态会从"待支付"变成"已支付",又可能变成"已发货",同步到 StarRocks 后只想查询最新状态,主键模型通常更符合这个目标。

需要注意的是,主键模型保留的是当前结果,不是完整变化过程。

如果既要当前状态,又要完整历史,一种常见做法是分别设计当前状态表和历史明细表,而不是要求一张表同时承担两种相反的目标。

聚合模型:相同聚合键按规则合并

聚合模型对应 AGGREGATE KEY。

它处理的重点不是"订单状态要不要替换",而是:相同维度的数据能不能提前合并。

假设我们不保存每笔订单,而是保存每天各城市的销售额增量:

apache 复制代码
2026-08-06  上海  100
2026-08-06  上海  200

如果日期和城市是聚合键,销售额使用 SUM,两次写入后可以得到:

apache 复制代码
2026-08-06  上海  300

建表语句可以简化成:

java 复制代码
CREATE TABLE city_sales_daily (
    sale_date DATE,
    city      VARCHAR(50),
    sales     DECIMAL(18, 2) SUM
)
AGGREGATE KEY(sale_date, city)
PARTITION BY date_trunc('month', sale_date)
DISTRIBUTED BY HASH(city);

其中:

  • sale_datecity 决定哪些数据属于同一组;

  • sales 后面的 SUM 决定同组指标怎样合并。

除了 SUM,聚合模型还支持其他聚合方式。初学阶段先抓住一点:写入的数据不一定原样保存,相同聚合键的指标会按照指定函数合并。

聚合模型适合查询模式比较固定、经常按同一组维度汇总,并且不需要还原每条原始明细的场景。

它换来的好处是减少后续查询需要处理的数据量;付出的代价是原始明细已经被聚合,不能再从这张表中完整找回来。

更新模型:结果也是最新值,但现在通常优先考虑主键模型

更新模型对应 UNIQUE KEY。

从查询结果看,它与主键模型很像:相同唯一键的新数据写入后,返回最新的一条。

js 复制代码
先写入:10001  待支付
再写入:10001  已支付
 
查询结果:10001  已支付

简化建表语句如下:

java 复制代码
CREATE TABLE orders_unique (
    order_id   BIGINT,
    status     VARCHAR(20),
    amount     DECIMAL(18, 2),
    update_time DATETIME
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id);

既然查询结果相似,为什么还同时存在更新模型和主键模型?

因为两者的底层实现不同。更新模型属于较早的更新方案,读取时需要合并不同版本的数据;主键模型通过新的存储引擎处理更新,在实时更新和查询性能之间做了更好的平衡。

StarRocks 官方目前也明确说明:更新模型正在逐渐被主键模型取代。

所以对于新建的实时更新表,如果没有特殊的历史兼容原因,通常应该先考虑主键模型。工作中已经存在的更新模型仍然需要认识和维护,但不必因为"四种模型"就认为四种方案同样推荐。

四种模型,真正的区别是什么

把建表语法先放到一边,四种模型最核心的区别可以放进同一张表:

表模型 相同 Key 再次写入 主要保留什么 常见场景
明细模型 新旧数据都保留 原始明细或历史过程 日志、事件、流水
主键模型 新数据替代旧数据 当前最新状态 订单、用户、库存、CDC 同步
聚合模型 按聚合函数合并 预聚合指标 日报、流量、销售汇总
更新模型 查询时返回最新数据 当前最新状态 已有的历史更新表

这里有一个特别实用的判断:

先问清楚同一个 Key 再来一条数据时,希望旧数据怎样处理,再决定表模型。

图 2:是否保留全部历史、是否需要最新状态、是否允许提前聚合,是选择表模型的主要入口。

工作中可以怎样选

如果把实际需求摆在面前,可以先按下面的顺序判断。

需要每一次原始记录吗

如果一条都不能丢,后续还要复盘完整行为过程,选择明细模型。

例如访问日志、点击事件、接口调用记录,每一行本身就是要分析的事实。

只关心每个对象现在的状态吗

如果相同 order_id、user_id 或 product_id 只想保留最新结果,而且数据会更新或删除,优先考虑主键模型。

从 MySQL 通过 CDC 同步订单、用户和商品数据,通常就属于这一类。

数据本来就是固定口径的汇总指标吗

如果只关心"某天、某城市的销售额"这类固定维度汇总,并且不需要回查每笔订单,可以考虑聚合模型。

但如果查询维度经常变化,或者后续可能需要原始明细,就不要过早把数据聚合掉。

为什么系统里还有更新模型

如果接手的已有表使用 UNIQUE KEY,就要按更新模型理解它。

如果是新建表,并且目标是保存最新状态,通常先评估主键模型,而不是为了把四种模型都用一遍而选择更新模型。

表模型和分区、分桶不是同一个选择

上一篇讲了分区和分桶,这里正好把两者接起来。

js 复制代码
表模型:决定相同 Key 的数据怎样处理
分区、分桶:决定数据怎样划分和分布

一张主键表仍然需要考虑分区和分桶;一张明细表也同样需要。

例如订单当前状态表可以先这样理解:

js 复制代码
主键模型:order_id 相同,只保留最新订单状态
分区:再根据常用时间范围划分数据
分桶:再考虑数据如何均匀分布和并行处理

三者各自解决不同问题,不能互相替代。真正组合建表时,还要满足模型对分区列、分桶列和 Key 的约束,这部分放到下一篇实战中再写。

最后,先记住这四个动作

如果暂时记不住完整建表语法,先记住相同 Key 再次写入时发生什么:

js 复制代码
明细模型:留
主键模型:替
聚合模型:合
更新模型:新

其中"新"表示查询时保留最新结果,但新建实时更新表通常优先考虑主键模型。

回到开头那笔订单:

  • 想看"待支付 → 已支付"的完整过程,用明细模型;

  • 只想看订单现在是"已支付",用主键模型;

  • 只想保存某天某城市的销售总额,用聚合模型;

  • 遇到已有 UNIQUE KEY 表,按更新模型理解和维护。

下一篇,我们把前面学过的内容真正组合起来:

从一张 MySQL 订单表开始,怎样选择表模型、分区和分桶,建出第一张能用于工作的 StarRocks 表?

相关推荐
朱峥嵘(朱髯)1 小时前
数据库如何根据全表 NDV 估算子集的 NDV
数据库·算法
Nturmoils2 小时前
MongoDB迁移:从烟囱式部署走向 KES-AI 时代融合数据库架构
数据库
深念Y2 小时前
hotspot-adb-research
linux·数据库·adb·emmc·随身wifi·移动终端
ERD Online2 小时前
我们怎么设计 good first issue:让第一个 PR 两小时内合入
数据库·git·后端·开源·issue
NeilYuen2 小时前
Mysql实战——图片读写
数据库·mysql
Sayuanni%32 小时前
微服务_注册中心Eureka与Nacos
数据库
倔强的石头_3 小时前
一次MongoDB迁移之后,我开始重新理解融合数据库
数据库
正在走向自律3 小时前
【金仓数据库征文】从 MySQL 迁移到金仓数据库:哈工大智能造价项目的一次信创改造实践
数据库·mysql·性能优化·国产数据库·数据库迁移·信创适配·金仓数据库征文
奇特認3 小时前
MySQL 集群技术 1.源码编译
数据库·mysql