开篇核心:数据模型是Doris表设计的根基
在上一篇中,我们掌握了Doris的FE/BE架构分工与分区分桶策略,而数据模型则是连接架构与业务的核心桥梁——它决定了数据如何存储、如何聚合、如何更新,直接影响查询性能与业务适配性。Apache Doris作为面向实时分析的MPP OLAP数据库,针对不同业务场景(明细存储、指标聚合、主键更新),设计了Duplicate Key、Aggregate Key、Unique Key三大核心数据模型,再配合丰富的数据类型支持,可满足从日志存储到主数据管理的全场景需求。本文将深入拆解三大模型的原理、适用场景与实践技巧,搭配数据类型详解,帮你快速完成Doris表设计选型,避开常见踩坑点。
1. 背景
1.1. 为什么需要多种数据模型?
Apache Doris 是一个面向实时分析的 MPP OLAP 数据库,其设计目标是在 高并发、低延迟 场景下高效处理 明细查询 与 聚合查询。不同业务场景对数据语义的要求不同:
- Duplicate Key 模型(明细模型):有些场景需要保留所有原始明细(如日志、行为事件)
- Aggregate Key 聚合模型:有些场景天然就是聚合态(如 PV/UV 汇总表)
- Unique Key 主键模型:有些场景要求主键唯一且支持更新(如用户画像、订单状态)
为此,Doris 提供了三种 建表时指定的数据模型(Data Model),通过 Sort Key(排序键) + 聚合语义 的组合,实现存储与计算的深度协同优化。
📌 注意:这三种模型
- 均基于 列式存储 + 排序 + 索引 架构
- 区别在于 写入时是否自动聚合 以及 如何处理重复主键
1.2. 排序键
Doris 中,数据按列进行排序存储,一张表可以分为 Key 列和 Value 列
- Key 列:用于分组与排序,可以是一个或多个字段
- Value 列:用于参与聚合
建表时,不同表类型中,Key 列不同
- 明细表(Duplicate):Key 列为 Aggreate Key,表示排序,没有唯一键的约束
- 主键表(Unique):Key 列为 Unique Key,表示排序和唯一键约束
- 聚合表(Aggregate):Key 列为 Aggregate Key,表示排序和唯一键约束
合理使用排序键的优势
- 加速查询性能:有助于减少数据的扫描量
- 数据压缩优化:有序存储提高压缩效率
- 减少去重成本:针对 Unique 表
1.3. Doris 中的表概述
创建表:CREATE TABLE,CREATE TABLE LIKE,CREATE TABLE AS
表名:默认大小写敏感,默认表名最大长度为 64 字节;均可配置
表属性:建表时可通过PROPERTIES指定,作用于分区,包括
- 分桶数(buckets):决定数据在表中的分布
- 存储介质(storage_medium):控制数据的存储方式,如使用 HDD、SSD 或远程共享存储
- 副本数 (replication_num):控制数据副本的数量,以保证数据的冗余和可靠性
- 冷热分离存储策略 (storage_policy) :控制数据的冷热分离存储的迁移策略
修改表属性只对未来创建的分区生效,对已经创建好的分区不生效
2. Duplicate Key 模型(明细模型)
2.1. 原理
- 不进行任何聚合,所有写入的行都作为独立记录存储。
- 表的 Sort Key(前缀列)仅用于排序和构建稀疏索引,不影响数据语义。
- 支持完全的
INSERT/DELETE/UPDATE(通过 Unique 模型替代或借助 Routine Load + Delete),但原生 Duplicate 模型本身 不保证主键唯一性。
2.2. 创建与使用
通过DUPLICATE KEY指定数据存储的排序列,建议选择三列或更少的列作为排序键
建表说明:
- 在建表时,可以通过
DUPLICATE KEY关键字指定明细表。 - 明细表必须指定数据的 Key 列,用于在存储时对数据进行排序。
CREATE TABLE IF NOT EXISTS example_tbl_duplicate ( log_time DATETIME NOT NULL, log_type INT NOT NULL, ... ) DUPLICATE KEY(log_time, log_type, error_code) DISTRIBUTED BY HASH(log_type) BUCKETS 10;数据插入与存储:
- 在明细表中,数据不进行去重与聚合,插入数据即存储数据,采用追加方式(Append)存储
INSERT INTO即追加数据
2.3. 适用场景
- 原始日志存储(如 Nginx 日志、APP 埋点)
- 需要保留全量明细 的分析场景(如漏斗分析、路径分析)
- 后续通过物化视图做聚合
2.4. 性能特点
- 写入吞吐高(无聚合开销)
- 查询可利用 Sort Key 做谓词下推和 Zone Map 跳过
- 存储空间较大(无去重/聚合压缩)
2.5. 实践建议
- Sort Key 应选择 高频过滤字段 + 高基数字段(如时间 + user_id)
- 避免将低基数字段放前面(导致索引效果差)
3. Aggregate Key 模型(聚合模型)
3.1. 原理
- 在写入时自动按 Sort Key 聚合,相同 Sort Key 的行会被合并。
- 非 Sort Key 的 Value 列必须指定 聚合函数(如
SUM、MAX、MIN、REPLACE、HLL_UNION、BITMAP_UNION) - 不支持
UPDATE / DELETE(因为语义是“不断累加”) - 聚合发生在 BE 节点本地 Compaction 阶段(非实时,但最终一致)
- 数据导入阶段:数据按批次导入,每批次生成一个版本,并对相同聚合键的数据进行初步聚合
- 后台文件合并阶段(Compaction):多个版本文件会定期合并,减少冗余并优化存储;
- 查询阶段:查询时,系统会聚合同一聚合键的数据,确保查询结果准确。
3.2. 创建与使用
使用AGGREGATE KEY关键字在建表时指定聚合表,并指定 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;数据导入时
- Key 列会聚合成一行
- Value 列会按照指定的聚合类型进行维度聚合。
Value 列,支持以下类型的维度聚合:
聚合方式 | 描述 |
| 求和,多行的 Value 进行累加。 |
| 替代,下一批数据中的 Value 会替换之前导入过的行中的 Value。 |
| 保留最大值。 |
| 保留最小值。 |
| 非空值替换。与 REPLACE 的区别在于对 值,不做替换。 |
| HLL 类型的列的聚合方式,通过 HyperLogLog 算法聚合。 |
| BITMAP 类型的列的聚合方式,进行位图的并集聚合。 |
3.3. 适用场景
- 预聚合报表(如每日站点 PV/UV)
- 指标汇总表(GMV、订单数等)
- 使用 HLL/Bitmap 做近似去重
3.4. 性能特点
- 极大减少存储空间(尤其对高重复率数据)
- 查询性能极高(无需运行时 GROUP BY)
- 写入时需等待 Compaction 才完成最终聚合(非强实时聚合)
3.5. 限制
在聚合列(Value)上,执行与聚合类型不一致的聚合类查询时,要注意语意。
- 比如
SELECT MIN(cost) FROM table cost列为SUM列,此时MIN的结果是MIN(SUM(cost)),而非直接导入的cost值
一致性保证,在某些查询中,会极大地降低查询效率。
- 针对基本的
COUNT(*),Doris 必须扫描所有的 AGGREGATE KEY 列,并且聚合后,才能得到语意正确的结果 - 当聚合列非常多时,
COUNT(*)查询需要扫描大量的数据
当业务上有频繁的COUNT(*)查询时
- 获取原始数据的导入行数:增加一个值恒为 1 的,聚合类型为
SUM的列
COUNT(*)等价于SUM(COUNT)- 后者的查询效率将远高于前者。
- 获取聚合后的行数:增加一个值恒为 1 的,聚合类型为
REPLACE的列
COUNT(*)等价于SUM(COUNT)- 后者的查询效率将远高于前者。
3.6. 实践建议
- 聚合列必须显式声明函数,不能混用普通列
- 避免在高基数维度上做聚合(会导致 Tablet 膨胀)
- 部分列更新的实现:如果使用聚合模型,而不使用聚合函数,所有 Value 列均为
REPLACE_IF_NOT_NULL,那么:Aggregate Key + REPLACE_IF_NOT_NULL = UNIQUE KEY,就可以使用 Doris 实现部分列更新
4. Unique Key 模型(主键模型)
这是 Doris 最复杂也最具演进性 的模型,经历了 Merge-on-Read → Merge-on-Write 的重大架构升级。
4.1. 原理演进
4.1.1. 旧版:Merge-on-Read(MoR)
- 写入时不合并,新旧版本共存。
- 查询时实时合并(读时合并):扫描所有版本,取最新(按 sequence col 或导入版本)。
- 依赖 Delete Bitmap 标记旧版本为“已删除”。
- 问题:查询性能差(需扫描多版本)、Compaction 压力大、内存消耗高。
- 场景:写多读少的场景,在查询是需要进行多个版本合并,谓词无法下推,可能会影响到查询速度
4.1.2. 新版(Doris 2.0+):Merge-on-Write(MoW)
🚀 这是 Doris 在 2023 年后最重要的存储引擎升级之一。
- 写入时直接覆盖旧数据,物理上只保留最新版本。
- 引入 主键索引(Primary Key Index):
- 基于 LSM-Tree + RocksDB / 内存 Hash Index(可选)
- 快速定位待更新行的物理位置(Row Location)
- 支持 真正的 UPSERT 和 DELETE,语义清晰。
- 查询时 无需合并,直接读取最新数据,性能接近 Duplicate 模型。
- 写时合并兼顾查询和写入性能,避免多个版本的数据合并,并支持谓词下推到存储层
4.2. 建表示例
4.2.1. 写时合并
在建表时
- 使用
UNIQUE KEY关键字:指定主键表 - 显式开启
enable_unique_key_merge_on_write属性:指定写时合并模式
自 Doris 2.1 版本以后,默认开启写时合并:
CREATE TABLE IF NOT EXISTS example_tbl_unique ( user_id LARGEINT NOT NULL, user_name VARCHAR(50) NOT NULL ) UNIQUE KEY(user_id, user_name) DISTRIBUTED BY HASH(user_id) BUCKETS 10 PROPERTIES ( "enable_unique_key_merge_on_write" = "true" );4.2.2. 读时合并
在建表时
- 使用
UNIQUE KEY关键字:指定主键表 - 显示关闭
enable_unique_key_merge_on_write属性:指定读时合并模式
在 Doris 2.1 版本之前,默认开启读时合并:
CREATE TABLE IF NOT EXISTS example_tbl_unique ( user_id LARGEINT NOT NULL, username VARCHAR(50) NOT NULL ) UNIQUE KEY(user_id, username) DISTRIBUTED BY HASH(user_id) BUCKETS 10 PROPERTIES ( "enable_unique_key_merge_on_write" = "false" );4.3. 适用场景
- 需要主键唯一约束 的业务表(如用户表、商品表)
- 频繁更新/删除 的场景(如订单状态变更、用户标签刷新)
- CDC(Change Data Capture) 数据同步(如 Debezium → Doris)
4.4. 性能特点(MoW vs MoR)
维度 | Merge-on-Read (旧) | Merge-on-Write (新) |
写入性能 | 高(追加写) | 中(需查主键索引) |
查询性能 | 低(多版本合并) | 高(单版本直读) |
存储空间 | 高(存多版本) | 低(仅最新版) |
更新延迟 | 最终一致 | 强一致(事务内可见) |
主键索引 | 无 | 有(RocksDB / MemIndex) |
4.5. 实践建议(MoW)
- 主键应尽量短且高基数(避免索引膨胀)
- 开启
light_schema_change支持快速加列 - 监控主键索引内存使用(可通过
SHOW PROC '/frontends'查看) - 写入吞吐略低于 Duplicate,但远优于 MoR 的查询性能
4.6. 最新进展(截至 Doris 2.1~2.2)
- 支持 Partial Update(部分列更新):只需提供主键 + 待更新列,其他列自动保留。
-- 只更新 age,name 保持不变 INSERT INTO user_profile(user_id, age) VALUES (1001, 28);- 支持 Sequence Column:指定一个列(如
update_time)作为版本依据,解决乱序更新问题。
PROPERTIES ( "function_column.sequence_type" = "DATETIME", "function_column.sequence_col" = "update_time" );- 主键索引支持 持久化到磁盘(RocksDB),避免 FE 内存压力过大。
5. 三种模型对比总结
特性 | Duplicate Key | Aggregate Key | Unique Key (MoW) |
数据语义 | 保留所有明细 | 自动聚合 | 主键唯一,支持更新 |
是否去重 | 否 | 是(按 Key) | 是(按主键) |
写入语义 | Append-only | Append + 后台聚合 | Upsert / Delete |
查询性能 | 中(需扫描明细) | 极高(预聚合) | 高(单版本) |
存储效率 | 低 | 高 | 中高 |
适用场景 | 日志、事件流 | 报表、指标汇总 | 主数据、CDC、画像 |
是否支持更新 | 否(需 Delete) | 否 | ✅ 是 |
最新能力 | - | Bitmap/HLL | Partial Update, Sequence Col |
6. 数据模型选型建议
- 不确定用哪种?先用 Duplicate Key
它最灵活,后续可通过 物化视图 派生 Aggregate 或 Unique 视图。 - 报表类需求 → Aggregate Key
如果业务天然就是“按天汇总”,直接建聚合表,省去运行时 GROUP BY。 - 需要更新主数据 → Unique Key + MoW
Doris 2.0+ 强烈推荐开启enable_unique_key_merge_on_write=true。 - 高并发点查(如用户画像)
Unique Key + 主键索引 + 合理分桶,可实现 <50ms 的 P99 延迟。 - 避免混合模型滥用
不要为了“既能明细又能聚合”而建多个表——用 物化视图自动同步 更优雅。
7. 数据类型
通过SHOW DATA TYPES;语句查看 Apache Doris 支持的所有数据类型。
7.1. 数值类型
类型名 | 存储空间(字节) | 描述 |
| 1 | 布尔值,0 代表 |
| 1 | 有符号整数,范围 [-128, 127]。 |
| 2 | 有符号整数,范围 [-32768, 32767]。 |
| 4 | 有符号整数,范围 [-2147483648, 2147483647] |
| 8 | 有符号整数,范围 [-9223372036854775808, 9223372036854775807]。 |
| 16 | 有符号整数,范围 [-2^127 + 1 ~ 2^127 - 1]。 |
| 4 | 浮点数,范围 [-3.410^38 ~ 3.410^38]。 |
| 8 | 浮点数,范围 [-1.7910^308 ~ 1.7910^308]。 |
| 4/8/16/32 | 高精度定点数,格式: |
注意:Doris 不支持unsigned
7.2. 日期类型
类型名 | 存储空间(字节) | 描述 |
| 4 | 日期类型,目前的取值范围是 ['0000-01-01', '9999-12-31'],默认的打印形式是 |
| 8 | 日期时间类型,格式: |
其他类型如Timestamp, Time, Year,Doris 不支持
7.3. 字符串类型
类型名 | 存储空间(字节) | 描述 |
| M | 定长字符串,M 代表的是定长字符串的字节长度。M 的范围是 1-255。 |
| 不定长 | 变长字符串,M 代表的是变长字符串的字节长度。M 的范围是 1-65533。变长字符串是以 UTF-8 编码存储的,因此通常英文字符占 1 个字节,中文字符占 3 个字节。 |
| 不定长 | 变长字符串,默认支持 1048576 字节(1MB),可调大到 2147483643 字节(2GB)。String 类型只能用在 Value 列,不能用在 Key 列和分区分桶列 |
7.4. 半结构类型
类型名 | 存储空间(字节) | 描述 |
| 不定长 | 由 T 类型元素组成的数组,不能作为 Key 列使用。目前支持在 Duplicate 和 Unique 模型的表中使用。 |
| 不定长 | 由 K, V 类型元素组成的 map,不能作为 Key 列使用。目前支持在 Duplicate 和 Unique 模型的表中使用。 |
| 不定长 | 由多个 Field 组成的结构体,也可被理解为多个列的集合。不能作为 Key 使用,目前 STRUCT 仅支持在 Duplicate 模型的表中使用。一个 Struct 中的 Field 的名字和数量固定,总是为 Nullable。 |
| 不定长 | 二进制 JSON 类型,采用二进制 JSON 格式存储,通过 JSON 函数访问 JSON 内部字段。长度限制和配置方式与 String 相同 |
| 不定长 | 动态可变数据类型,专为半结构化数据如 JSON 设计,可以存入任意 JSON,自动将 JSON 中的字段拆分成子列存储,提升存储效率和查询分析性能。长度限制和配置方式与 String 相同。Variant 类型只能用在 Value 列,不能用在 Key 列和分区分桶列。 |
7.5. IP 类型
类型名 | 存储空间(字节) | 描述 |
| 4 字节 | 以 4 字节二进制存储 IPv4 地址,配合 ipv4_* 系列函数使用。 |
| 16 字节 | 以 16 字节二进制存储 IPv6 地址,配合 ipv6_* 系列函数使用。 |