上一篇讲完分区和分桶后,我们知道了一行数据会怎样分布到不同 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_date、city决定哪些数据属于同一组; -
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 表?