2. 明细模型(Duplicate Key Model)
2.1 模型原理:说白了就是“原样存储”
明细模型,英文叫 Duplicate Key Model。它的核心思想很简单——你写入什么,我就存什么。不做任何聚合,不做任何去重,也不做任何预计算。
我刚开始接触 StarRocks 时,觉得这模型太“笨”了。后来才发现,恰恰是这种“笨”,让它成了最灵活、最通用的模型。你想想看,如果数据本身就不需要聚合,或者你还没想好要怎么分析,那明细模型就是最安全的选择。
它的底层原理是这样的:
- 每一行数据都是一个独立的记录
- 用户指定的 Duplicate Key 只用于排序,不用于去重
- 同一批次或不同批次写入的完全相同的行,都会被保留
- 数据按照 Duplicate Key 的顺序在磁盘上存储,这叫“前缀索引”
关键区别:明细模型没有“主键”概念。你指定的那些列,只是“排序列”,不是“唯一标识”。这是它和主键模型(Primary Key Model)最本质的区别。
我画了一张图,帮你理解明细模型的数据流向:
2.2 适用场景:什么时候选它?
说实话,我见过不少团队一上来就选聚合模型,结果后面需求变了,想查明细却查不到,只能重建表。所以我的建议是——如果你不确定,先用明细模型。
具体来说,这几类场景特别适合:
- 日志存储与分析:用户行为日志、访问日志、操作日志。每条记录都是独立的,不需要合并
- 原始数据保留:金融交易流水、订单记录。每一笔都不能丢,也不能被覆盖
- 全量数据导出:需要把数据原样导出到其他系统做二次处理
- 任意维度即席查询:你还没想好固定的分析维度,需要保留所有字段供后续探索
- 数据审计与回溯:需要查看某个时间点的原始数据状态
我的经验:有一次做用户行为分析平台,业务方一开始说只要 PV/UV 聚合。我留了个心眼,底层用了明细模型。三个月后,他们要做漏斗分析,需要看每个用户的完整行为路径。如果当时用了聚合模型,数据就全废了。所以,明细模型是“后悔药”。
2.3 建表语法:动手写一个
建表语法其实很直观。我直接给你看一个完整的例子:
CREATE TABLE IF NOT EXISTS user_behavior_log (
event_id BIGINT COMMENT '事件ID',
user_id VARCHAR(50) COMMENT '用户ID',
event_type VARCHAR(20) COMMENT '事件类型',
event_time DATETIME COMMENT '事件时间',
page_url VARCHAR(500) COMMENT '页面URL',
duration_sec INT COMMENT '停留时长(秒)',
extra_info JSON COMMENT '扩展信息'
) ENGINE = OLAP
DUPLICATE KEY(event_id, event_time)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"storage_medium" = "SSD",
"storage_cooldown_time" = "2025-12-31 23:59:59"
);
这里有几个要点,我拆开来说:
- DUPLICATE KEY(event_id, event_time):指定排序列。数据会按 event_id 和 event_time 排序存储。注意,这里不是主键,只是排序用的
- DISTRIBUTED BY HASH(user_id):分桶键。我习惯用高基数字段做分桶,比如 user_id,这样数据分布更均匀
- BUCKETS 16:分桶数。一般建议是集群节点数的 3~5 倍
- replication_num = "3":副本数。生产环境至少 3 副本,保证高可用
⚠️ 注意:DUPLICATE KEY 中指定的列,在建表后不能修改。所以建表前一定要想清楚哪些列是常用的过滤条件。我见过有人把 20 个字段全放进 DUPLICATE KEY,结果前缀索引完全失效,查询性能惨不忍睹。
2.4 数据写入:几种常见方式
明细模型对写入非常友好。因为它不做任何额外处理,写入性能是所有模型中最高的。我常用的写入方式有这几种:
2.4.1 INSERT INTO 单条写入
INSERT INTO user_behavior_log VALUES
(1001, 'user_001', 'page_view', '2025-01-15 10:30:00', '/home', 45, NULL);
2.4.2 批量写入
INSERT INTO user_behavior_log VALUES
(1002, 'user_002', 'click', '2025-01-15 10:31:00', '/product/123', 12, '{"source":"search"}'),
(1003, 'user_003', 'purchase', '2025-01-15 10:32:00', '/checkout', 120, '{"amount":299.00}'),
(1004, 'user_001', 'page_view', '2025-01-15 10:33:00', '/cart', 30, NULL);
2.4.3 Stream Load(推荐方式)
生产环境我基本都用 Stream Load,性能最好:
curl --location-trusted -u root: -H "label:behavior_log_20250115" \
-H "column_separator:," \
-T /data/logs/behavior_20250115.csv \
http://fe_host:8030/api/example_db/user_behavior_log/_stream_load
小技巧:Stream Load 的 label 要保证唯一性,这样即使任务失败重试,也不会产生重复数据。我习惯用「表名_日期_批次号」的命名方式。
2.5 查询示例:看看效果
明细模型的查询,说白了就是直接查原始数据。我写几个典型场景:
2.5.1 查询某个用户的所有行为
SELECT * FROM user_behavior_log
WHERE user_id = 'user_001'
ORDER BY event_time
LIMIT 100;
这个查询会走前缀索引,因为 event_time 是 DUPLICATE KEY 的第二列。如果只查 user_id,那就是全表扫描了。所以建表时,要把最常用的过滤字段放在 DUPLICATE KEY 的前面。
2.5.2 统计某段时间内的事件分布
SELECT event_type, COUNT(*) AS cnt
FROM user_behavior_log
WHERE event_time >= '2025-01-15 00:00:00'
AND event_time < '2025-01-16 00:00:00'
GROUP BY event_type
ORDER BY cnt DESC;
你看,明细模型也支持 GROUP BY。它是在查询时实时聚合的,不是预聚合。数据量大的时候,性能会受点影响,但胜在灵活。
2.5.3 查看重复数据(明细模型特有的操作)
SELECT event_id, user_id, event_time, COUNT(*) AS dup_cnt
FROM user_behavior_log
GROUP BY event_id, user_id, event_time
HAVING COUNT(*) > 1;
这个查询在聚合模型里是查不到的,因为数据已经被合并了。但在明细模型里,你可以清楚地看到哪些数据被重复写入了。我曾经用这个功能帮业务方排查过数据管道的问题,非常实用。
2.6 性能调优要点
嗯,这里我要说几个实际中容易踩的坑:
| 优化项 | 建议 | 原因 |
|---|---|---|
| DUPLICATE KEY 列数 | 不超过 3~5 列 | 列数太多会导致前缀索引失效,排序开销大 |
| 分桶键选择 | 选高基数字段 | 数据分布均匀,避免数据倾斜 |
| 分桶数 | 节点数 × 3~5 | 充分利用并行查询能力 |
| 查询过滤 | 尽量用 DUPLICATE KEY 前列 | 走前缀索引,避免全表扫描 |
| 批量写入大小 | 每批 10万~100万行 | 太小则导入频繁,太大则内存压力大 |
⚠️ 我曾经踩过的坑:有次我把 10 个字段都放进了 DUPLICATE KEY,想着“反正都能排序”。结果查询时发现,只要过滤条件不是前几个字段,性能就极差。后来查文档才知道,StarRocks 的前缀索引只取 DUPLICATE KEY 的前 36 字节。超过的部分,索引就失效了。所以,DUPLICATE KEY 要精不要多。
2.7 小结
明细模型是 StarRocks 最基础的模型,也是我用的最多的模型。它的核心价值就一句话:保留数据的原始面貌。
选它的时候,你不需要纠结“数据要不要聚合”、“要不要去重”。你只需要想清楚:这些数据以后会不会需要看原始记录?如果答案是“会”,那就用明细模型。
我个人习惯是:除非有非常明确的聚合需求,否则一律先用明细模型。等业务稳定了,再考虑要不要换成其他模型。毕竟,从明细模型导出数据做聚合很容易,但从聚合模型还原明细数据,那可就难了。