大数据Doris之_1_数据模型
前言
Github:https://github.com/HealerJean
一、模型概述
在
Doris中建表时需要指定表模型,以定义数据存储与管理方式。在Doris中提供了明细模型、聚合模型以及 主键模型 三种表模型,可以应对不同的应用场景需求。不同的表模型具有相应的数据去重、聚合及更新机制。选择合适的表模型有助于实现业务目标,同时保证数据处理的灵活性和高效性。
| 明细模型 | 主键模型 | 聚合模型 | |
|---|---|---|---|
Key 列唯一约束 |
不支持,Key 列可以重复 |
支持 | 支持 |
| 同步物化视图 | 支持 | 支持 | 支持 |
| 异步物化视图 | 支持 | 支持 | 支持 |
UPDATE 语句 |
不支持 | 支持 | 不支持 |
DELETE 语句 |
部分支持 | 支持 | 部分支持 |
| 导入时整行更新 | 不支持 | 支持 | 不支持 |
| 导入时部分列更新 | 不支持 | 支持 | 部分支持 |
1、表模型分类
1)[明细模型](Duplicate Key Model)
允许指定的
Key列重复,Doirs存储层保留所有写入的数据,适用于必须保留所有原始数据记录的情况;
- 适合任意维度的
Ad-hoc查询。虽然同样无法利用预聚合的特性,但是不受聚合模型的约束,可以发挥列存模型的优势(只读取相关列,而不需要读取所有Key列)。 - 如同 “数据仓库的杂货铺”,货物(数据)按类别(列)整齐摆放,顾客(查询)可随意挑选任意商品组合,灵活但需要现场打包(实时计算)。
2)[主键模型](Unique Key Model)
每一行的
Key值唯一,可确保给定的Key列不会存在重复行,Doris存储层对每个key只保留最新写入的数据,适用于数据更新的情况;
- 针对需要唯一主键约束的场景,可以保证主键唯一性约束。但是无法利用
ROLLUP等预聚合带来的查询优势。 - 如同 “图书馆的借书系统”,每本书(记录)有唯一编号(主键),方便快速查找,但无法直接统计 “某类书的总数量”(需额外计算)。
3)[聚合模型](Aggregate Key Model)
可根据
Key列聚合数据,Doris存储层保留聚合后的数据,从而可以减少存储空间和提升查询性能;通常用于需要汇总或聚合信息(如总数或平均值)的情况。
- 可以通过预聚合,极大地降低聚合查询时所需扫描的数据量和查询的计算量,非常适合有固定模式的报表类查询场景。但是该模型对
count(*)查询很不友好。同时因为固定了Value列上的聚合方式,在进行其他类型的聚合查询时,需要考虑语意正确性。 - 如同 “超市的预包装食品”,提前按类别(维度)打包称重(预聚合),适合快速购买固定套餐(固定报表查询),但无法临时更换包装内的商品(灵活聚合)
2、排序键
在
Doris中,数据以列的形式存储,一张表可以分为
key列与value列。其中,key列用于分组与排序,value列用于参与聚合。Key列可以是一个或多个字段,在建表时,按照各种表模型中,AggregateKey、UniqueKey和DuplicateKey的列进行数据排序存储。
不同的表模型都需要在建表时指定 Key 列,分别有不同的意义:
- 对于
DuplicateKey模型,Key列表示排序,没有唯一键的约束。 - 在
AggregateKey与UniqueKey模型中,会基于Key列进行聚合,Key列既有排序的能力,又有唯一键的约束。
1)排序键-收益
- 加速查询性能:
- 减少数据扫描:排序键有助于减少数据扫描量。
- 定位数据位置:对于范围查询或过滤查询,可以利用排序键直接定位数据的位置。
- 加速数据排序:对于需要进行排序的查询,也可以利用排序键进行排序加速;
-
数据压缩优化:数据 按排序键有序存储会提高压缩的效率,相似的数据会聚集在一起,压缩率会大幅度提高,从而减小数据的存储空间。
- 减少去重成本:当使用
UniqueKey表时,通过排序键,Doris能更有效地进行去重操作,保证数据唯一性。
2)排序键-建议
选择排序键时,可以遵循以下建议:
Key列必须在所有Value列之前。- 尽量选择整型类型。因为整型类型的计算和查找效率远高于字符串。
- 对于不同长度的整型类型的选择原则,遵循够用即可。
- 对于
VARCHAR和STRING类型的长度,遵循够用即可原则。
二、明细模型(Duplicate Key Model)
明细模型是
Doris中的默认建表模型,用于保存每条原始数据记录。在建表时,通过DUPLICATE KEY指定数据存储的排序列,以优化常用查询。一般建议选择三列或更少的列作为排序键,具体选择方式参考[排序键]。明细模型具有以下特点:
1、特点:
- 保留原始数据:明细模型保留了全量的原始数据,适合于存储与查询原始数据。对于需要进行详细数据分析的应用场景,建议使用明细模型,以避免数据丢失的风险;
- 不去重也不聚合:与聚合模型与主键模型不同,明细模型不会对数据进行去重与聚合操作。即使两条相同的数据,每次插入时也会被完整保留;
- 灵活的数据查询:明细模型保留了全量的原始数据,可以从完整数据中提取细节,基于全量数据做任意维度的聚合操作,从而进行元数数据的审计及细粒度的分析。
2、使用场景
一般明细模型中的数据只进行追加,旧数据不会更新。明细模型适用于需要存储全量原始数据的场景:
- 日志存储:用于存储各类的程序操作日志,如访问日志、错误日志等。每一条数据都需要被详细记录,方便后续的审计与分析;
- 用户行为数据:在分析用户行为时,如点击数据、用户访问轨迹等,需要保留用户的详细行为,方便后续构建用户画像及对行为路径进行详细分析;
- 交易数据:在某些存储交易行为或订单数据时,交易结束时一般不会发生数据变更。明细模型适合保留这一类交易信息,不遗漏任意一笔记录,方便对交易进行精确的对账。
3、建表说明
在建表时,可以通过
DUPLICATE KEY关键字指定明细模型。明细表必须指定数据的Key列,用于在存储时对数据进行排序。下例的明细表中存储了日志信息,并针对于log_time、log_type及error_code三列进行了排序:
CREATE TABLE IF NOT EXISTS example_tbl_duplicate
(
log_time DATETIME NOT NULL,
log_type INT NOT NULL,
error_code INT,
error_msg VARCHAR(1024),
op_id BIGINT,
op_time DATETIME
)
DUPLICATE KEY(log_time, log_type, error_code)
DISTRIBUTED BY HASH(log_type) BUCKETS 10;
4、数据插入与存储
在明细表中,数据不进行去重与聚合,插入数据即存储数据。明细模型中
Key列指做为排序。

在上例中,表中原有 4 行数据,插入 2 行数据后,采用追加(APPEND)方式存储,共计 6 行数据:
-- 4 rows raw data
INSERT INTO example_tbl_duplicate VALUES
('2024-11-01 00:00:00', 2, 2, 'timeout', 12, '2024-11-01 01:00:00'),
('2024-11-02 00:00:00', 1, 2, 'success', 13, '2024-11-02 01:00:00'),
('2024-11-03 00:00:00', 2, 2, 'unknown', 13, '2024-11-03 01:00:00'),
('2024-11-04 00:00:00', 2, 2, 'unknown', 12, '2024-11-04 01:00:00');
-- insert into 2 rows
INSERT INTO example_tbl_duplicate VALUES
('2024-11-01 00:00:00', 2, 2, 'timeout', 12, '2024-11-01 01:00:00'),
('2024-11-01 00:00:00', 2, 2, 'unknown', 13, '2024-11-01 01:00:00');
-- check the rows of table
SELECT * FROM example_tbl_duplicate;
+---------------------+----------+------------+-----------+-------+---------------------+
| log_time | log_type | error_code | error_msg | op_id | op_time |
+---------------------+----------+------------+-----------+-------+---------------------+
| 2024-11-02 00:00:00 | 1 | 2 | success | 13 | 2024-11-02 01:00:00 |
| 2024-11-01 00:00:00 | 2 | 2 | timeout | 12 | 2024-11-01 01:00:00 |
| 2024-11-03 00:00:00 | 2 | 2 | unknown | 13 | 2024-11-03 01:00:00 |
| 2024-11-04 00:00:00 | 2 | 2 | unknown | 12 | 2024-11-04 01:00:00 |
| 2024-11-01 00:00:00 | 2 | 2 | unknown | 13 | 2024-11-01 01:00:00 |
| 2024-11-01 00:00:00 | 2 | 2 | timeout | 12 | 2024-11-01 01:00:00 |
+---------------------+----------+------------+-----------+-------+---------------------+
三、主键模型
当需要更新数据时,可以选择主键模型(
UniqueKeyModel)。该模型保证Key列的唯一性,插入或更新数据时,新数据会覆盖具有相同Key的旧数据,确保数据记录为最新。与其他数据模型相比,主键模型适用于数据的更新场景,在插入过程中进行主键级别的更新覆盖。
1、特点
- 基于主键进行
UPSERT:在插入数据时,主键重复的数据会更新,主键不存在的记录会插入; - 基于主键进行去重:主键模型中的
Key列具有唯一性,会对根据主键列对数据进行去重操作; - 高频数据更新:支持高频数据更新场景,同时平衡数据更新性能与查询性能。
2、使用场景
- 部分列更新:如画像标签场景需要变更频繁改动的动态标签,消费订单场景需要改变交易的状态。通过主键模型部分列更新能力可以完成某几列的变更操作。
- 高频数据更新:适用于上游
OLTP数据库中的维度表,实时同步更新记录,并高效执行UPSERT操作; - 数据高效去重:如广告投放和客户关系管理系统中,使用主键模型可以基于用户
ID高效去重;
3、实现方式
在
Doris中主键模型有两种实现方式:
1)写时合并(merge-on-write)
自
1.2版本起,Doris默认使用写时合并模式,数据在写入时立即合并相同Key的记录,确保存储的始终是最新数据。写时合并兼顾查询和写入性能,避免多个版本的数据合并,并支持谓词下推到存储层。大多数场景推荐使用此模式;
在建表时,使用 UNIQUE KEY 关键字可以指定主键表。通过显示开启 enable_unique_key_merge_on_write 属性可以指定写时合并模式。自 Doris 1.2 版本以后,默认开启写时合并:
CREATE TABLE IF NOT EXISTS example_tbl_unique
(
user_id LARGEINT NOT NULL,
user_name VARCHAR(50) NOT NULL,
city VARCHAR(20),
age SMALLINT,
sex TINYINT
)
UNIQUE KEY(user_id, user_name)
DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES (
"enable_unique_key_merge_on_write" = "true"
);
2)读时合并(merge-on-read)
在
1.2版本前,Doris中的主键模型默认使用读时合并模式,数据在写入时并不进行合并,以增量的方式被追加存储,在Doris内保留多个版本。查询或Compaction时,会对数据进行相同Key的版本合并。读时合并适合写多读少的场景,在查询是需要进行多个版本合并,谓词无法下推,可能会影响到查询速度。
在建表时,使用 UNIQUE KEY 关键字可以指定主键表。通过显示关闭 enable_unique_key_merge_on_write 属性可以指定读时合并模式。在 Doris 1.2 版本之前,默认开启读时合并:
CREATE TABLE IF NOT EXISTS example_tbl_unique
(
user_id LARGEINT NOT NULL,
username VARCHAR(50) NOT NULL,
city VARCHAR(20),
age SMALLINT,
sex TINYINT
)
UNIQUE KEY(user_id, username)
DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES (
"enable_unique_key_merge_on_write" = "false"
);
3)更新方式
- 整行更新:
UniqueKey模型默认的更新语义为整行UPSERT,即UPDATEORINSERT,该行数据的Key如果存在,则进行更新,如果不存在,则进行新数据插入。在整行UPSERT语义下,即使用户使用InsertInto指定部分列进行写入,Doris也会在Planner中将未提供的列使用NULL值或者默认值进行填充。 - 部分列更新:如果用户希望更新部分字段,需要使用写时合并实现,并通过特定的参数来开启部分列更新的支持。
4、数据插入与存储
在主键表中,
Key列不仅用于排序,还用于去重,插入数据时,相同Key的记录会被覆盖。

如上例所示,原表中有 4行数据,插入 2 行后,新插入的数据基于主键进行了更新:
-- insert into raw data
INSERT INTO example_tbl_unique VALUES
(101, 'Tom', 'BJ', 26, 1),
(102, 'Jason', 'BJ', 27, 1),
(103, 'Juice', 'SH', 20, 2),
(104, 'Olivia', 'SZ', 22, 2);
-- insert into data to update by key
INSERT INTO example_tbl_unique VALUES
(101, 'Tom', 'BJ', 27, 1),
(102, 'Jason', 'SH', 28, 1);
-- check updated data
SELECT * FROM example_tbl_unique;
+---------+----------+------+------+------+
| user_id | username | city | age | sex |
+---------+----------+------+------+------+
| 101 | Tom | BJ | 27 | 1 |
| 102 | Jason | SH | 28 | 1 |
| 104 | Olivia | SZ | 22 | 2 |
| 103 | Juice | SH | 20 | 2 |
+---------+----------+------+------+------+
5、注意事项
Unique表的实现方式只能在建表时确定,无法通过schemachange进行修改;- 在整行
UPSERT语义下,即使用户使用insertinto指定部分列进行写入,Doris也会在Planner中将未提供的列使用NULL值或者默认值进行填充; - 部分列更新。如果用户希望更新部分字段,需要使用写时合并实现,并通过特定的参数来开启部分列更新的支持。
- 使用
Unique表时,为了保证数据的唯一性,分区键必须包含在Key列内。
四、聚合模型
Doris的聚合模型专为高效处理大规模数据查询中的聚合操作设计。它通过预聚合数据,减少重复计算,提升查询性能。聚合模型只存储聚合后的数据,节省存储空间并加速查询。
1、使用场景
- 明细数据进行汇总:用于电商平台的月销售业绩、金融风控的客户交易总额、广告投放的点击量等业务场景中,进行多维度汇总;
- 不需要查询原始明细数据:如驾驶舱报表、用户交易行为分析等,原始数据存储在数据湖中,仅需存储汇总后的数据。
2、原理
每一次数据导入会在聚合模型内形成一个版本,在 Compaction 阶段进行版本合并,在查询时会按照主键进行数据聚合:
- 数据导入阶段:数据按批次导入,每批次生成一个版本,并对相同聚合键的数据进行初步聚合(如求和、计数);
- 后台文件合并阶段(
Compaction):多个版本文件会定期合并,减少冗余并优化存储; - 查询阶段:查询时,系统会聚合同一聚合键的数据,确保查询结果准确。
3、建表说明
使用
AGGREGATEKEY关键字在建表时指定聚合模型,并指定Key列用于聚合Value列。
CREATE TABLE IF NOT EXISTS example_tbl_agg
(
user_id LARGEINT NOT NULL,
load_dt DATE NOT NULL,
city VARCHAR(20),
last_visit_dt DATETIME REPLACE DEFAULT "1970-01-01 00:00:00",
cost BIGINT SUM DEFAULT "0",
max_dwell INT MAX DEFAULT "0",
)
AGGREGATE KEY(user_id, load_dt, city)
DISTRIBUTED BY HASH(user_id) BUCKETS 10;
上例中定义了用户信息和访问行为表,将 user_id、load_date、city 及 age 作为 Key 列进行聚合。数据导入时,Key 列会聚合成一行,Value 列会按照指定的聚合类型进行维度聚合。
在聚合表中支持以下类型的维度聚合:
| 聚合方式 | 描述 |
|---|---|
SUM |
求和,多行的 Value 进行累加。 |
REPLACE |
替代,下一批数据中的 Value 会替换之前导入过的行中的 Value。 |
MAX |
保留最大值。 |
MIN |
保留最小值。 |
REPLACE_IF_NOT_NULL |
非空值替换。与 REPLACE 的区别在于对 null 值,不做替换。 |
HLL_UNION |
HLL 类型的列的聚合方式,通过 HyperLogLog 算法聚合。 |
BITMAP_UNION |
BITMAP 类型的列的聚合方式,进行位图的并集聚合。 |
4、数据插入与存储
在聚合表中,数据基于主键进行聚合操作。数据插入后及完成聚合操作。

在上例中,表中原有 4 行数据,在插入 2 行数据后,基于 Key 列进行维度列的聚合操作:
-- 4 rows raw data
INSERT INTO example_tbl_agg VALUES
(101, '2024-11-01', 'BJ', '2024-10-29', 10, 20),
(102, '2024-10-30', 'BJ', '2024-10-29', 20, 20),
(101, '2024-10-30', 'BJ', '2024-10-28', 5, 40),
(101, '2024-10-30', 'SH', '2024-10-29', 10, 20);
-- insert into 2 rows
INSERT INTO example_tbl_agg VALUES
(101, '2024-11-01', 'BJ', '2024-10-30', 20, 10),
(102, '2024-11-01', 'BJ', '2024-10-30', 10, 30);
-- check the rows of table
SELECT * FROM example_tbl_agg;
+---------+------------+------+---------------------+------+----------------+
| user_id | load_date | city | last_visit_date | cost | max_dwell_time |
+---------+------------+------+---------------------+------+----------------+
| 102 | 2024-10-30 | BJ | 2024-10-29 00:00:00 | 20 | 20 |
| 102 | 2024-11-01 | BJ | 2024-10-30 00:00:00 | 10 | 30 |
| 101 | 2024-10-30 | BJ | 2024-10-28 00:00:00 | 5 | 40 |
| 101 | 2024-10-30 | SH | 2024-10-29 00:00:00 | 10 | 20 |
| 101 | 2024-11-01 | BJ | 2024-10-30 00:00:00 | 30 | 20 |
+---------+------------+------+---------------------+------+----------------+
FAQ
1、明细模型
1)明细模型 为什么适合任意维度的 Ad-hoc 查询
-
灵活性: 不限制查询维度的组合,用户可自由选择任意列(包括非
Key列)作为查询条件,无需受限于预定义的聚合模型,比如Duplicate模型没有聚合模型count的这个局限性。因为该模型不涉及聚合语意,在做count(*)查询时,任意选择一列查询,即可得到语意正确的结果。。 -
列存优势: 列存数据库会按列独立存储数据。当查询仅涉及部分列时,可仅读取相关列的数据,避免扫描全量
Key列,大幅减少IO开销。举例:表结构为Key = (date, region),Value=(sales, users),若查询 “2025 年 6 月华东地区的用户数”,列存模型只需读取date、region、users三列,而非所有Key列。
2) 明细模型 为什么无法利用预聚合?
答案:预聚合需要提前定义聚合规则(如按时间、地区聚合销售额),而 Duplicate Key 模型不预设聚合逻辑,数据以原始粒度存储,因此无法通过预计算减少查询时的计算量。
2、主键模型
a:主键模型 为什么适合唯一主键约束场景?
- 数据唯一性保障:例如用户表中
user_id作为UniqueKey,可避免重复用户数据插入。 - 主键索引:通过唯一键建立索引,加速等值查询( 如
WHERE user_id=123)。
b:主键模型 为什么无法利用 ROLLUP 等预聚合?
答案:Unique Key 模型的核心是唯一性,数据以单条记录粒度存储,未进行预聚合,因此无法利用ROLLUP 优化查询。例如:若表以user_id为 Unique Key,存储用户明细数据,无法直接通过 ROLLUP 快速获取 “各地区用户数”,需实时聚合计算。
3、聚合模型
聚合模型的局限性本质是 空间效率与查询效率的权衡:
- 通过预聚合减少存储成本,但牺牲了部分查询场景的便捷性和性能(如
COUNT(*))。- 业务设计时需根据查询模式选择合适的模型(如聚合模型适用于
OLAP分析,而非频繁COUNT(*)的场景)
1)未聚合数据的查询一致性问题
在聚合模型中,模型对外展现的,是最终聚合后的数据。也就是说,任何还未聚合的数据(比如说两个不同导入批次的数据),不同导入批次的同源数据(如同一
user_id+date)在物理存储中是独立的,需通过查询时聚合算子确保对外展示结果的一致性。
假设表结构如下:
ColumnName |
Type |
AggregationType |
Comment |
|---|---|---|---|
| user_id | LARGEINT | 用户 id | |
| date | DATE | 数据灌入日期 | |
| cost | BIGINT | SUM | 用户总消费 |
假设存储引擎中有如下两个已经导入完成的批次的数据:
batch 1
| user_id | date | cost |
|---|---|---|
| 10001 | 2017/11/20 | 50 |
| 10002 | 2017/11/21 | 39 |
batch 2
| user_id | date | cost |
|---|---|---|
| 10001 | 2017/11/20 | 1 |
| 10001 | 2017/11/21 | 5 |
| 10003 | 2017/11/22 | 22 |
可以看到,用户 10001 分属在两个导入批次中的数据还没有聚合。但是为了保证用户只能查询到如下最终聚合后的数据:
| user_id | date | cost |
|---|---|---|
| 10001 | 2017/11/20 | 51 |
| 10001 | 2017/11/21 | 5 |
| 10002 | 2017/11/21 | 39 |
| 10003 | 2017/11/22 | 22 |
我们在查询引擎中加入了聚合算子,来保证数据对外的一致性。
另外,在聚合列(Value)上,执行与聚合类型不一致的聚合类查询时,要注意语意。比如在如上示例中执行如下查询:
SELECT MIN(cost) FROM table;
得到的结果是 5,而不是 1。实现代价:一致性保证依赖查询引擎实时聚合,增加了查询处理的计算开销。
2)聚合函数与模型定义的兼容性问题
当查询的聚合函数与表定义的
AggregationType不匹配时,结果可能不符合预期。
如上:对 cost 列(定义为SUM)执行 MIN(cost) 查询,结果为聚合后的最小值(5 ),而非原始数据中的最小值(1)
3)COUNT(*) 查询的效率瓶颈
性能问题:当聚合键列较多时,
COUNT(*)需扫描大量数据,导致查询效率显著下降。
select count(*) from table; 的正确结果应该为 4
-
传统数据库:可通过 统计信息 或 单列扫描快速计算
COUNT(*),因为在实现上,我们可以通过如“导入时对行进行计数,保存count的统计信息”,或者在查询时“仅扫描某一列数据,获得count值 -
聚合模型中:
COUNT(*)需扫描所有聚合键(AGGREGATE KEY)列,并执行实时聚合,原因如下:- 若仅扫描部分列,可能因未聚合的重复键导致结果错误(如案例中仅扫描
user_id会误判行数为3)。 - 若不进行查询时聚合,会返回原始行数(案例中为
5),与实际聚合结果(4行)不符。 - 正确结果:
4,必须同时读取user_id和date这两列的数据,再加上查询时聚合,才能返回 4 这个正确的结果,也就是说,在count(*)查询中,Doris必须扫描所有的AGGREGATEKEY列(这里就是user_iddate),并且聚合后,才能得到语意正确的结果。* 当聚合列非常多时,count(*)查询需要扫描大量的数据。
- 若仅扫描部分列,可能因未聚合的重复键导致结果错误(如案例中仅扫描
- 方案一:添加
SUM类型的计数列
- 操作:新增
count列(值恒为 1,聚合类型SUM),用SUM(count)替代COUNT(*)。 - 效率优势:仅需扫描单列,避免扫描所有聚合键列。
- 局限性:若存在重复导入(相同聚合键的多行数据),
SUM(count)会累加重复行,与COUNT(*)语义不一致。
- 方案二:添加
REPLACE类型的计数列
- 操作:
count列聚合类型改为REPLACE(值恒为 1)。 - 优势:无论是否重复导入,
SUM(count)结果始终等于COUNT(*)(因重复行的count会被替换为 1)。 - 注意:需确保
count列值始终为 1,避免语义错误。
4)为什么适合固定模式的报表类查询?
- 预聚合优化:数据写入时已按
Key维度聚合,查询时只需扫描聚合后的少量数据,大幅减少计算量。例:表结构为Key=(date, region),Value=SUM(sales),查询 “2025 年 6 月各地区销售额” 时,直接读取预聚合的结果,无需扫描全量明细数据。 - 固定模式适配: 报表通常有固定的查询维度(如时间、地区、产品类别),预聚合模型可提前优化这些场景的查询性能。
5)为什么会限制灵活性?
答案:写入时已确定 Value 列的聚合方式(如SUM 销售额),若查询时需要其他聚合方式(如 AVG、COUNT DISTINCT),可能导致语义错误或需要额外计算。
例:若 Value 列按 SUM 预聚合,当需要查询 “各地区平均销售额” 时,需用 SUM (sales) / COUNT (unique_users ),但预聚合中未存储用户数,可能需要关联其他表或重新计算。


