一、Doris是什么?—— 宏观定位

Apache Doris是一个基于MPP架构的、高性能、实时的分析型数据库

通过拆解这个定义,我们可以抓住它的核心:

  1. MPP架构:这是Doris高性能的基石。大规模并行处理意味着它将数据和计算任务分布到集群的多个节点上,这些节点同时工作,最终将结果汇总返回。就像一群工人同时组装一台汽车,而不是一个人完成所有工作。
  2. 分析型数据库:它与我们熟知的MySQL这类事务型数据库有本质区别。
    • 目标不同:OLTP处理大量简单的、高并发的短事务(如订单支付、账户查询);而Doris所属的OLAP则专注于复杂的分析查询(如“过去一年每个季度各个品类的销售总额排名”)。
    • 读/写模式不同:OLTP是“读写均衡”,OLAP是“读多写少”,一次写入,多次复杂查询。
  3. 高性能与实时:Doris的核心优势。它能够对海量数据(PB级别)进行亚秒级的查询响应,并支持高并发的点查询和复杂报表。数据写入后可立即用于查询,实现了“实时分析”。

简单来说,Doris是为大数据时代的“决策支持系统”而生的引擎,旨在让分析师和决策者能够极其快速地从海量数据中获取洞察。

二、核心架构解析—— 它是如何工作的?

Doris采用了一种非常经典且简洁的主从架构,主要由两类进程组成:

1. Frontend

负责元数据管理、客户端连接、查询的解析与规划。它又分为两个角色:

  • **Leader:**主节点,负责所有元数据的更新和全局管理。
  • **Follower:**从节点,参与选主,并同步元数据,分担读压力。

你可以将FE理解为集群的“大脑”和“调度中心”

2. Backend

负责数据存储、查询执行。每个BE节点存储一部分数据(Tablet),并执行FE下发的查询任务。

你可以将BE理解为集群的“肌肉”和“劳动力”

工作流程(以一次查询为例):

  1. 客户端向任意一个FE发送SQL查询。
  2. FE解析SQL,生成分布式执行计划(查询规划)。
  3. FE将执行计划分发给涉及到的所有BE节点。
  4. 各个BE节点并行执行本地数据的扫描和计算(这就是MPP的威力)。
  5. BE将中间结果返回给某个或多个BE进行进一步聚合(如果需要)。
  6. 最终结果由FE汇总后返回给客户端。

这种架构的优势是组件简单、易于运维、没有单点瓶颈(FE通过选举实现高可用)。

三、数据模型详解—— 设计的灵魂

这是Doris最核心的特性之一,我们在上一个问题中已详细讨论,这里再强调其设计哲学:

与传统数据库(如MySQL)的“行式存储”和“读时计算”不同,Doris作为一种OLAP数据库,其核心思想是:牺牲一定的写时灵活性,换取极致的查询性能。实现这一目标的手段就是 “预聚合”

这三种模型,本质上是在 “存储成本”、“查询性能”、“数据粒度”三者之间进行不同权衡的结果。

模型 设计哲学 类比 典型场景
明细模型 保留一切。提供最大的灵活性,但存储和计算成本最高。 保存所有原始发票存根。 日志分析、Ad-hoc任意维度查询、需要回溯明细。
聚合模型 空间换时间。通过预聚合,将计算提前到写入时,换取查询时的极致性能。 每天只保存一张销售日报表。 固定维度的报表、Dashboard、大数据量汇总分析。
唯一模型 关注最新状态。确保主键唯一,保留最新版本,是聚合模型的特例。 只记录每个客户的当前联系信息和总消费额。 实时用户画像、商品信息表、需要去重的流式数据。

选择诀窍:优先考虑聚合模型,因为它能带来最大的性能收益。只有在必须查看原始明细时,才使用明细模型。

我们可以用一个简单的比喻来理解:

  • 明细数据就像一堆堆的 “原材料”
  • 查询就像是客户要的 “成品菜”(比如 SUM、COUNT 后的结果)。

1. 明细模型 - 存储“原材料”

  • 思想:保留最原始、最细粒度的数据,不做任何预聚合。查询时现场加工。
  • 原理:就是普通的数据库表,每一行数据都会独立存储。插入相同key的数据,会存在多行。
  • 特点:
  • 存储成本最高:存储的是原始数据。
  • 查询灵活性最高:可以查询任意维度的最细粒度数据。
  • 查询性能相对较低:每次查询都需要现场进行聚合计算。

2. 聚合模型 - 存储“半成品”

  • 思想:在数据写入时,根据AGGREGATE KEY自动进行预聚合,将相同key的数据行合并,并对Value列执行指定的聚合操作(如SUM、MAX)。
  • 原理:
    • 建表时,需要指定AGGREGATE KEY,并为非key列指定聚合函数(如SUMMAXMINREPLACE)。
    • 当导入数据的AGGREGATE KEY与已有数据相同时,Doris不会新增一行,而是会将新数据的Value列与旧数据按指定的聚合函数进行合并
  • 特点:
    • 存储成本显著降低:相同key的数据被合并,大大减少了数据量。
    • 查询性能极高:对于聚合查询,结果几乎是瞬间得出,因为大部分计算在写入时已经完成。
    • 灵活性受限:无法查询到被合并前的、最细粒度的明细数据。

3. 唯一模型 - 存储“最新版本”

  • 思想:保证UNIQUE KEY的唯一性,对于相同key的数据,保留最新版本(或根据指定策略保留一个版本)。它本质上是聚合模型的一个特例。
  • 原理:
    • 建表时指定UNIQUE KEY。所有非key列默认使用REPLACE聚合函数。
    • 当新数据与旧数据有相同UNIQUE KEY时,新数据会覆盖旧数据(REPLACE语义)。
  • 特点:
    • 存储成本中等:只保留唯一key的最新状态,但比聚合模型存储的细节多。
    • 查询性能高:点查询(查询某个key的最新状态)非常快。
    • 适用场景特定:主要用于需要实时获取某个对象最新状态的场景。

如何判断与配置?

决策流程图:如何选择模型?

当你拿到一份数据时,可以遵循以下决策流程:

image-20251106235013103

Key和Value的配置原则

  • Key列(维度列):
    • 作用:用于过滤、分组、关联。决定了数据如何被聚合(聚合/唯一模型)或标识(明细模型)。
    • 如何选:选择那些经常出现在 WHEREGROUP BYJOIN ... ON条件中的列。应将高基数列(如user_id)放在Key列的前面,以提高查询效率。
  • Value列(指标列):
    • 作用:用于度量和计算。在聚合/唯一模型中,它们需要被聚合。
    • 如何配置:
      • SUM:适用于可累加的指标,如销售额点击次数
      • MAX/MIN:适用于记录极值,如最高温度最低价格
      • REPLACE:适用于“覆盖更新”的场景,在唯一模型中很常见,如用户最后登录时间。在聚合模型中,可用于保留最新明细。
      • HLL_UNION:用于近似计算唯一值计数(COUNT DISTINCT),如UV
      • BITMAP_UNION:用于精确计算唯一值计数,如UV
      • NONE:仅在明细模型中使用,表示不聚合。

实例验证:电商场景实战

假设我们有如下结构的原始订单流水数据:

1
(order_id, user_id, product_id, category, province, order_time, amount, quantity)

场景1:实时订单分析平台

  • 需求:业务方需要能下钻查询任意一笔订单的详细信息,比如排查某个订单的支付问题。
  • 模型选择:
    • 需要查询最细粒度数据 -> 明细模型
  • 建表语句:
1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE order_detail (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
category VARCHAR(50),
province VARCHAR(20),
order_time DATETIME,
amount DECIMAL(10,2),
quantity INT
)
DUPLICATE KEY(order_id, user_id, order_time) -- 明细模型,KEY无聚合意义,仅用于排序
DISTRIBUTED BY HASH(order_id) BUCKETS 10;

场景2:销售大盘报表

  • 需求:每天查看按品类省份汇总的销售总额订单总数。不需要看单个订单。
  • 模型选择:
    • 查询模式是固定维度的聚合 -> 聚合模型
  • 建表语句:
1
2
3
4
5
6
7
8
9
CREATE TABLE order_agg (
category VARCHAR(50),
province VARCHAR(20),
dt DATE, -- 增加一个日期维度,按天聚合
total_amount DECIMAL(10,2) SUM, -- 销售额需要累加
order_count BIGINT SUM -- 订单数需要累加
)
AGGREGATE KEY(category, province, dt) -- 聚合键,相同维度的数据会被合并
DISTRIBUTED BY HASH(category, province) BUCKETS 10;
  • 效果:原始1000万条订单数据,按天聚合后可能只剩下1万条数据,查询速度快百倍。

场景3:实时用户画像表

  • 需求:需要快速查询每个用户的最新状态,如最近一次登录时间当前会员等级总消费金额
  • 模型选择:
    • 需要获取每个用户的最新状态 -> 唯一模型
  • 建表语句:
1
2
3
4
5
6
7
8
CREATE TABLE user_profile (
user_id BIGINT,
last_login_time DATETIME REPLACE, -- 最后登录时间,新的覆盖旧的
membership_level INT REPLACE, -- 会员等级,新的覆盖旧的
total_expense DECIMAL(10,2) SUM -- 总消费金额,需要持续累加
)
UNIQUE KEY(user_id) -- 唯一键,保证一个用户一条记录
DISTRIBUTED BY HASH(user_id) BUCKETS 10;
  • 效果:无论用户数据更新多少次,表中每个user_id都只对应一条最新状态的记录。

四、核心特性与优势—— 为什么是Doris?

  1. 极致的性能:
    • 列式存储:数据按列存储,查询时只需读取相关列,极大减少I/O。
    • 智能索引:
      • 前缀索引:数据按AGGREGATE/UNIQUE KEY排序存储,查询时能快速定位数据块。
      • ZoneMap索引:每个数据块记录min/max值,用于快速过滤。
      • Bloom Filter索引:高效判断数据块中是否包含某个值,适合高基数列。
    • 物化视图:可以自动匹配查询,无需改写SQL即可加速。
  2. 简便的运维:
    • 兼容MySQL协议:可以使用任何MySQL客户端连接,学习成本极低。
    • 简洁的架构:只有FE和BE两种组件,部署和运维非常简单。
    • 在线弹性扩缩容:增加节点后,数据会自动均衡,无需停机。
  3. 丰富的生态支持:
    • 多种数据导入方式:支持Broker Load(HDFS)、Routine Load(Kafka)、Stream Load(HTTP API)等,方便与现有大数据生态集成。
    • 支持标准SQL:降低了数据分析师的使用门槛。

五、实践指南:如何上手一个项目?

1. 表设计流程

  1. 分析业务需求:查询模式是什么?需要明细还是聚合?并发量多大?
  2. 选择数据模型:根据上述决策流程图选择。
  3. 定义Key和Value:确定维度列和指标列,并为指标列选择合适的聚合函数。
  4. 分区与分桶:
    • 分区:通常按时间分区(如dt),用于数据生命周期管理和查询时裁剪。
    • 分桶:按高基数列(如user_id)分桶,保证数据均匀分布,实现并行计算。
  5. 设置索引:对常用查询条件列创建Bloom Filter索引。

2. 实战示例:用户行为分析平台

需求:分析用户在不同渠道的日活跃、消费情况。

建表语句(聚合模型):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE user_behavior_agg (
dt DATE COMMENT '日期', -- 分区列,也是维度
user_id BIGINT COMMENT '用户ID', -- 高基数维度
channel VARCHAR(50) COMMENT '渠道', -- 维度
country VARCHAR(20) COMMENT '国家', -- 维度

-- 以下是指标列(Value),使用聚合函数
pv BIGINT SUM COMMENT '总页面浏览量', -- 可累加
uv BIGINT BITMAP_UNION COMMENT '独立用户数', -- 使用BITMAP精确去重计数
total_amount DECIMAL(16,2) SUM COMMENT '总消费金额', -- 可累加
last_login_time DATETIME MAX COMMENT '最后登录时间' -- 取最大值
)
ENGINE=OLAP
AGGREGATE KEY(dt, user_id, channel, country) -- 聚合键
PARTITION BY RANGE(dt) -- 按日期分区,便于管理
(
PARTITION p202401 VALUES LESS THAN ("2024-02-01")
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32 -- 按user_id分桶,保证数据均匀
PROPERTIES (
"storage_format" = "v2"
);

查询示例:

1
2
3
4
5
6
7
8
9
10
-- 查询2024年1月各渠道的日活跃用户和总消费
SELECT
dt,
channel,
BITMAP_UNION_COUNT(uv) AS daily_uv, -- 计算BITMAP的COUNT
SUM(total_amount) AS daily_amount
FROM user_behavior_agg
WHERE dt >= '2024-01-01' AND dt <= '2024-01-31'
GROUP BY dt, channel
ORDER BY dt, channel;

这个查询会非常快,因为uvtotal_amount在数据写入时已经预聚合好了。


六、总结:Doris的适用场景与定位

Doris非常适合作为:

  • 实时数据仓库:替代传统的Hive/Spark离线数仓,实现T+0的实时分析。
  • 统一查询网关:通过对上游数据库(如MySQL)进行CDC同步,提供复杂的关联分析能力。
  • 日志分析与监控平台:替代ELK方案,提供更强的实时聚合分析和SQL能力。
  • 高性能报表与BI系统:为Tableau、Superset等BI工具提供亚秒级响应的数据源。

Doris可能不适用于:

  • 高频、小事务的OLTP场景(请用MySQL/PostgreSQL)。
  • 超大规模(数十PB级)的离线批处理(Hive/Spark仍有成本优势)。

总而言之,Doris以其卓越的性能、简易的运维和强大的实时能力,已经成为现代数据栈中不可或缺的核心组件。掌握Doris,意味着你拥有了解决大规模实时数据分析问题的利器。

七、分桶的目的是什么?(为什么要把数据分开装?)

你可以把Doris的表想象成一个大仓库,分桶就是把仓库划分成一个个有编号的货架

分桶的核心目的有三个:

  1. 并行计算与查询加速(最主要目的)
    • Doris是一个分布式系统,数据会被分散到多个服务器上。一个分桶是数据移动和并行计算的最小单元
    • 当执行一个查询时,比如 SELECT user_id, SUM(amount) FROM sales GROUP BY user_id,Doris可以将这个查询任务拆分成多个子任务。每个子任务在一个分桶的数据上独立执行(比如每个机器节点处理自己持有的几个分桶),最后将结果汇总。这极大地利用了集群的计算能力,加快了查询速度。
  2. 数据均匀分布,避免数据倾斜
    • 如果没有分桶,数据可能随机或者不均匀地分布在集群中,导致某些机器负载过高(数据热点),而其他机器闲置。
    • 通过合理选择分桶键,可以确保数据尽可能均匀地分布到各个分桶中,从而让集群的负载更加均衡,避免单点瓶颈。
  3. 优化数据导入和压缩
    • 数据按分桶键组织后,相似的数据会聚集在一起(特别是当分桶键和排序键一致时)。
    • 这有利于:
      • 增量导入:新数据可以高效地路由到对应的分桶。
      • 数据压缩:相似的数据压缩率更高。

八、分桶键如何选择?(按什么规则来分货架?)

分桶键的选择是Doris表设计中最重要的决策之一。基本原则是:选择高基数列(即唯一值多的列),并且该列经常作为查询的过滤或分组条件。

以下是优先级和建议:

  1. 首选:经常用于查询过滤或关联(JOIN)条件的列。
    • 例如:user_id, order_id, device_id
    • 原因:当查询条件中包含分桶键时,Doris可以快速定位到相关的分桶,跳过无关分桶,大幅减少需要扫描的数据量(这叫“分桶裁剪”)。如果两个表使用相同的分桶键进行JOIN,还可以进行本地关联,效率极高。
  2. 次选:经常用于分组(GROUP BY)或聚合的列。
    • 例如:在用户行为分析中,经常按 user_id做聚合。
    • 原因:相同分桶键的数据会落在同一个分桶内,进行局部聚合时效率更高。
  3. 避免选择低基数列(重复值多的列)。
    • 例如:性别(gender)省份(province)状态(status)
    • 原因:如果分桶数大于这些列的唯一值数量,会导致数据倾斜。比如按“性别”分桶,最多只能有效利用2个分桶,其他分桶可能是空的。

最佳实践:

  • 如果查询模式多样,没有绝对突出的列,可以选择唯一ID列,如 user_id,因为它能最好地保证数据均匀分布。
  • 可以采用多个列的组合作为分桶键,但通常单个高基数列就足够了。

九、分桶数如何设置?(要划分多少个货架?)

分桶数决定了数据的“并行度”。设置不当会严重影响性能。

设置原则:

  1. 目标:单个分桶的数据量在100MB ~ 1GB之间为宜。
    • 太小(< 100MB):会产生大量小文件,增加元数据管理开销,影响查询调度效率。
    • 太大(> 1GB):则并行度不够,无法充分利用集群资源,导入和查询时可能成为瓶颈。
  2. 计算方法:
    • 估算表的总数据量:比如,一张表预计一年有 1TB 的数据。
    • 计算分桶数:分桶数 = 总数据量 / 单个分桶目标大小
      • 按100MB算:1TB / 100MB = 1024 * 100MB / 100MB = 1024个分桶
      • 按1GB算:1TB / 1GB = 1024个分桶 / 10 = 102.4(约100-110个分桶)
    • 这是一个预估值,需要根据数据增长进行调整。
  3. 考虑集群规模:
    • 分桶数最好是集群机器节点数量的整数倍(如 1倍, 2倍)。
    • 例如,你有10台BE(Doris的后端节点),那么设置分桶数为 10, 20, 30… 这样可以保证每个BE上的分桶数量基本一致,负载均衡。

举例说明:

假设你为一张“订单明细表”进行设计:

  • 表数据量:预计每月增长100GB,希望保留12个月,总数据量约1.2TB。
  • 查询模式:90%的查询都包含 user_id条件。
  • 集群规模:有20台BE服务器。

设计决策:

  1. 分桶键:选择 user_id。因为它既是高基数列,又是最常用的查询条件。
  2. 分桶数:
    • 按1GB/桶计算:1200GB / 1GB ≈ 1200个桶。
    • 考虑集群有20台BE,1200 / 20 = 60个桶/每台。这个数是可以接受的。
    • 为了让分布更均匀,我们可以取一个接近1200且是20的倍数的数,比如 1200本身。
    • 最终的建表语句如下:
1
2
3
4
5
6
7
8
9
10
CREATE TABLE order_detail (
user_id BIGINT,
order_id BIGINT,
product_id BIGINT,
amount DECIMAL(10,2),
order_time DATETIME
)
ENGINE=OLAP
AGGREGATE KEY(user_id, order_id, product_id, order_time) -- 聚合模型
DISTRIBUTED BY HASH(user_id) BUCKETS 1200; -- 按user_id的Hash值分桶,共1200个桶

总结

问题 核心答案 关键要点
分桶目的 并行计算、负载均衡、优化IO 分桶是分布式并行计算的最小单元。
分桶键选择 高基数、常用作查询条件/关联键 首选user_id这类列,避免gender
分桶数设置 保证单桶100MB-1GB,是BE节点数的整数倍 通过总数据量/目标大小估算,并考虑集群规模。

记住这个口诀:“键选高基常用列,桶数参考总量与节点,单桶大小百兆为佳”。掌握了这些,你就能设计出高效的Doris表了。