Doris 学习(二):底层架构与三种数据模型深入解析

上一篇从全局视角介绍了 Doris 的定位、安装和性能对比,知道了它是"FE + BE 两个进程、亚秒级查询、支持实时写入"的分析数据库。本篇把视角下沉到内核,讲清楚两件事:底层架构到底是怎么运转的 ,以及三种数据模型(明细、聚合、主键)的内部实现和选型逻辑。


一、底层架构总览

Doris 的架构设计目标是"极简运维 + 极致性能",整个系统只有两类进程,但每个进程内部承担了完整的职责。

复制代码
┌─────────────────────────────────────────────────────┐
│                      Client                         │
│          MySQL 协议 / HTTP Stream Load               │
└──────────────────────┬──────────────────────────────┘
                       │
          ┌────────────▼────────────┐
          │          FE 集群         │
          │  Leader / Follower / Obs │
          └────────────┬────────────┘
                       │ 1. 元数据读写  2. SQL 解析优化  3. 查询计划下发
          ┌────────────▼────────────┐
          │          BE 集群         │
          │  Tablet / Segment / MemTable │
          │  向量化执行 / Compaction    │
          └─────────────────────────┘

下面分别拆开 FE 和 BE 的内部结构。


二、FE 内部架构

FE 是 Doris 的"大脑",承担四类核心职责:元数据管理、SQL 解析与优化、查询调度、集群管理。

1. 元数据管理

FE 把整个集群的元数据(库、表、分区、副本、作业、权限等)存在内存中,并通过 BDBJE(Berkeley DB Java Edition) 持久化。

元数据采用 写前日志(Edit Log)+ 快照(Image) 的机制:

  • 所有元数据变更先写 Edit Log,再更新内存;
  • 定期把内存元数据 dump 成 Image 文件,加速重启恢复;
  • FE 之间通过 Edit Log 同步,保证元数据一致。

FE 角色与一致性:

角色 职责 数量
Leader 写元数据、生成查询计划、下发查询 1 个
Follower 参与 Paxos 投票,可被选为 Leader 至少 2 个(组成奇数台)
Observer 只读元数据,不参与投票 任意,用于读扩展

元数据写入走 Paxos 协议,多数 Follower 确认后才算成功,保证强一致。

2. SQL 解析与优化

一条 SQL 从进 FE 到下发执行,经过这条链路:

复制代码
SQL 文本
   │
   ▼ 1. 解析(Parser)
语法树 AST
   │
   ▼ 2. 分析(Analyzer)
绑定库表、列、类型,生成逻辑计划
   │
   ▼ 3. 逻辑优化(Logical Optimizer)
谓词下推、列裁剪、子查询展开、常量折叠
   │
   ▼ 4. 物理优化(CBO Optimizer)
基于代价选择 Join 顺序、Join 算法、数据分布
   │
   ▼ 5. 生成分布式物理计划
切成多个 Fragment,每个 Fragment 有数据分布要求

CBO(Cost-Based Optimizer) 是 FE 的核心,基于统计信息(行数、NDV、列的 min/max)估算每个执行计划的代价,选取代价最低的。统计信息通过 ANALYZE TABLE 收集。

3. 查询调度

FE 把物理计划切成多个 Fragment,每个 Fragment 是一个可并行执行的单元。调度时:

  1. 计算每个 Fragment 需要扫描的 Tablet;
  2. 按数据本地性把 Fragment 调度到对应副本所在的 BE;
  3. BE 之间通过 Data Stream 流式交换中间数据(类似 Spark 的 Shuffle,但不落盘);
  4. 最顶层 Fragment 的结果汇聚到 FE,返回客户端。

4. 集群管理

FE 负责管理 BE 的生命周期:

  • BE 启动后向 FE 注册,FE 周期性发送心跳;
  • FE 维护 Tablet 到 BE 副本的映射;
  • 副本缺失时自动补副本,副本不均衡时自动迁移。

三、BE 内部架构

BE 是 Doris 的"手脚",负责数据存储和查询执行。可以拆成三层:存储引擎、执行引擎、写入引擎。

1. 存储引擎

BE 上的数据按 Tablet → Segment → Column → Page 组织:

复制代码
Tablet(分桶后的逻辑分片)
  ├── Segment 0        一批数据写入生成一个 Segment 文件
  │     ├── Column A   每列独立存储
  │     │     ├── Page 1   按 Page 压缩存储
  │     │     ├── Page 2
  │     │     └── Index Page(ZoneMap 等)
  │     ├── Column B
  │     └── Short Key Index(稀疏索引)
  ├── Segment 1
  └── ...

各层级作用:

  • Tablet:分桶的物理体现,是数据分布和副本的基本单位,每个 Tablet 默认 3 副本,分布在不同 BE;
  • Segment:一次批量写入生成的不可变文件,类似 LSM-Tree 的 SSTable;
  • Column:列式存储,每列连续存放,查询只读取需要的列;
  • Page:列内按 Page(默认 64KB)切分,每个 Page 独立压缩(LZ4/ZSTD),并有 ZoneMap(min/max/null 统计)。

稀疏索引(Short Key Index):

  • 按 Key 列排序,每 1024 行(可配置)记录一条索引项,指向某个 Page;
  • 查询时通过二分查找定位 Page,只需 2-3 次 I/O 就能找到起始位置;
  • Key 列长度超过 36 字节会被截断,所以 Key 不宜过长。

2. Compaction

由于 Segment 是不可变的,随着写入增多,Segment 数量会膨胀,查询时需要合并多个 Segment,影响性能。BE 后台会做 Compaction:

  • Cumulative Compaction:合并多个小 Segment 成一个大 Segment,减少 Segment 数量;
  • Base Compaction:把增量 Segment 合并到底部的 Base Segment,清理被删除/更新的数据。

Compaction 是 Doris 性能的关键,写入量大时要关注 Compaction 是否跟上,否则查询会越来越慢。

3. 执行引擎

BE 采用向量化执行引擎,核心特征:

  • 列式批量处理:每次处理 1024 行(block size),而不是逐行;
  • SIMD 加速:利用 CPU 的向量化指令(AVX2/SSE)并行处理一批数据;
  • 延迟物化(Late Materialization):先通过过滤条件定位行号,再读取需要的列,减少 I/O;
  • Runtime Filter:Join 时把小表的 Join Key 下推到大表扫描阶段,提前过滤数据,大幅减少 Shuffle 数据量。

支持的算子:Scan、Filter、Project、Aggregate、Hash Join、Sort、Window Function 等,全部向量化实现。

4. 写入引擎

写入流程以 Stream Load 为例:

复制代码
Client
  │ HTTP PUT
  ▼
FE(鉴权、计算 Tablet 分布)
  │ 重定向到某个 BE
  ▼
BE 接收数据
  │ 解析、按分桶列 Hash 计算目标 Tablet
  ▼
写入各 Tablet 对应的 MemTable(内存)
  │ 达到阈值(默认 100MB 或 10 秒)
  ▼
Flush 成 Segment 文件,写入磁盘
  │
  ▼
生成新版本号,元数据上报 FE

关键特性:

  • MemTable:写入先在内存聚合,达到阈值再刷盘,减少小文件;
  • 两阶段提交:FE 协调所有相关 BE,要么全部成功要么全部回滚,保证事务性;
  • 版本管理:每次写入生成一个新版本号,查询时读取对应版本的可见数据。

四、三种数据模型深入解析

数据模型是 Doris 区别于其他 OLAP 数据库的核心特性,建表时通过 DUPLICATE KEY、AGGREGATE KEY、UNIQUE KEY 指定,一旦创建不可修改。

模型总览

模型 关键词 数据处理方式 适用场景
明细模型 DUPLICATE KEY 原样存储所有行 日志、流水、明细查询
聚合模型 AGGREGATE KEY 相同 Key 的行按聚合函数合并 报表、预聚合、指标汇总
主键模型 UNIQUE KEY 相同主键的行覆盖(最新一行生效) 实时更新、点查、维表

下面分别深入讲。


1. 明细模型(DUPLICATE KEY)

原理 :数据原样存储,不做任何聚合,相同 Key 的行会保留多行。DUPLICATE KEY 只决定排序和索引,不决定去重。

sql 复制代码
CREATE TABLE detail_log (
    dt DATETIME,
    user_id BIGINT,
    event VARCHAR(50),
    cost INT
)
DUPLICATE KEY(dt, user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 16;

内部行为:

  • 数据按 DUPLICATE KEY 的列排序存储,稀疏索引基于这些列;
  • 插入两行 (dt=A, user_id=1, ...) 和 (dt=A, user_id=1, ...),两行都会保留;
  • 查询时扫描所有满足条件的行。

适用场景:

  • 需要保留每条原始记录的场景:用户行为日志、交易流水、系统日志;
  • 查询模式是聚合统计或明细过滤,不需要行级更新。

注意 :DUPLICATE KEY 的列必须是表的前几列,且按查询最常用的过滤维度排序,最大化稀疏索引和 ZoneMap 的效果。


2. 聚合模型(AGGREGATE KEY)

原理 :相同 AGGREGATE KEY 的行,在写入和查询时会按指定的聚合函数自动合并。非 Key 列必须指定聚合类型。

支持的聚合函数:

聚合类型 说明
SUM 求和
REPLACE 取最后写入的值
REPLACE_IF_NOT_NULL 非空时替换
MAX 取最大值
MIN 取最小值
HLL_UNION HLL 集合合并(用于去重计数)
BITMAP_UNION Bitmap 合并(用于精确去重)
sql 复制代码
CREATE TABLE sales_agg (
    dt DATE,
    city VARCHAR(20),
    amount DECIMAL(10,2) SUM,
    order_cnt INT SUM,
    max_amount DECIMAL(10,2) MAX
)
AGGREGATE KEY(dt, city)
DISTRIBUTED BY HASH(city) BUCKETS 8;

内部行为:

  • 写入时:相同 Key 的行在 MemTable 内、Segment 内、Compaction 时逐级聚合;
  • 查询时:如果查询的列都是聚合列,直接返回聚合结果,无需再扫描原始行;
  • REPLACE 类型依赖写入顺序,不能保证取的是"最新"业务意义上的值,只保证是最后写入的。

适用场景:

  • 报表预聚合:同维度的指标提前汇总,查询时直接出结果;
  • 去重计数:用 HLL_UNION 或 BITMAP_UNION 实现超大数据量的快速去重;
  • 实时汇总:数据持续写入,自动按维度聚合。

核心优势:查询极快,因为数据已经在存储层聚合好了,扫描量极小。

注意事项:

  • 聚合模型不支持查询非聚合的原始明细,一旦选择就无法回退;
  • REPLACE 在并发写入时结果不确定,需要业务保证顺序;
  • 维度列(Key)不能被聚合,如果维度值会变化,不适合聚合模型。

3. 主键模型(UNIQUE KEY)

原理 :相同 UNIQUE KEY 的行,最新一行覆盖旧行,实现行级更新。这是 Doris 支持实时更新的核心模型。

sql 复制代码
CREATE TABLE user_profile (
    user_id BIGINT,
    name VARCHAR(50),
    age INT,
    city VARCHAR(20)
)
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true"
);

两种实现模式:

模式 配置 写时行为 读时行为 性能
Merge-on-Read(默认旧) 关闭 直接写入新行,不删除旧行 查询时合并,保留最新 写快读慢
Merge-on-Write(推荐) enable_unique_key_merge_on_write=true 写入时标记旧行删除,新行写入 直接读新行 写稍慢读快

强烈建议开启 Merge-on-Write ,它通过主键索引实现:

  • 维护一个主键到行号的索引;
  • 写入新数据时,通过索引找到旧行并标记删除;
  • 查询时跳过被删除的行,只读最新数据;
  • 读性能接近明细模型,同时支持高效更新。

适用场景:

  • 需要实时更新的维表(用户画像、商品信息);
  • 需要高并发点查的场景(按主键查单条);
  • 数据会频繁变更,不能用聚合模型的场景。

支持的操作:

sql 复制代码
-- 覆盖写入(相同主键更新)
INSERT INTO user_profile VALUES (1, 'Alice', 20, 'Beijing');
INSERT INTO user_profile VALUES (1, 'Alice', 21, 'Shanghai');  -- age 变为 21,city 变为 Shanghai

-- 部分列更新(需要指定要更新的列)
INSERT INTO user_profile (user_id, age) VALUES (1, 22)
-- 只更新 age,name 和 city 保持不变

注意事项:

  • 主键列不能被更新,只能更新非主键列;
  • 部分列更新需要保证写入数据只包含主键和要更新的列;
  • enable_unique_key_merge_on_write 在建表时指定,建表后不可修改。

五、三种模型的对比与选型

维度 明细模型 聚合模型 主键模型
存储方式 原样存储 自动聚合 最新行覆盖
支持更新 否(只能追加) 否(只能追加聚合) 是(行级更新)
支持点查 弱(全表扫描) 弱 强(主键索引)
查询性能 取决于扫描量 极快(预聚合) 快
写入性能 快 快(写入时聚合) 中(写时合并开销)
存储空间 大(存所有明细) 小(聚合后数据少) 中(存最新行+删除标记)
典型场景 日志、流水 报表、指标 维表、实时画像

选型决策树:

复制代码
数据需要按主键更新吗?
├── 是 → 主键模型(UNIQUE KEY)
└── 否 → 查询需要看原始明细吗?
         ├── 是 → 明细模型(DUPLICATE KEY)
         └── 否(只看聚合指标)→ 聚合模型(AGGREGATE KEY)

常见组合用法:

  • 明细模型存原始日志,定时同步到聚合模型做报表加速;
  • 主键模型存用户维表,和明细表做 Join;
  • 聚合模型用 BITMAP_UNION 做用户去重指标,查询时直接 BITMAP_UNION_COUNT。

六、典型坑点

坑点 1:建表时选错模型,后续无法修改

数据模型在建表时确定,不能通过 ALTER TABLE 修改。选错了只能重建表、重导数据。

对策:建表前按上面的决策树确认需求,不确定时优先选明细模型(最灵活)。

坑点 2:聚合模型 REPLACE 取值不确定

REPLACE 只保证"最后写入的生效",但分布式写入时"最后"不保证是业务上的最新。

对策 :需要"取最新"用主键模型;聚合模型的 REPLACE 只适合覆盖全量导入的场景。

坑点 3:主键模型没有开启 Merge-on-Write

默认是 Merge-on-Read,查询时合并,数据量大时查询慢。

对策 :建表时一定加 PROPERTIES("enable_unique_key_merge_on_write" = "true")。

坑点 4:明细模型的 Key 列顺序不合理

稀疏索引只基于 DUPLICATE KEY 的列,如果查询常用的过滤条件不在 Key 列的前几位,索引就用不上。

对策:把最常用的过滤列放在 Key 的最前面,如时间列 + 高基数 ID。

坑点 5:主键模型部分列更新失败

部分列更新需要写入数据只包含主键和要更新的列,多带了列会用 NULL 覆盖原值。

对策 :部分更新时严格只写主键 + 更新列,或开启 partial_columns_mode=true 让系统自动处理。

坑点 6:聚合模型无法查询明细

聚合模型的数据在存储层已经被合并,无法查出原始行。

对策:如果既要明细又要聚合,用两张表(一张明细、一张聚合),明细导入时同步聚合到聚合表。


七、本篇小结

这一篇抓住两个核心:

  1. 底层架构:

    • FE 是大脑:元数据(BDBJE + Paxos)、SQL 优化(CBO)、查询调度、集群管理;
    • BE 是手脚:存储引擎(Tablet → Segment → Column → Page + 稀疏索引 + ZoneMap)、向量化执行引擎、MemTable 写入 + Compaction;
    • 写入走 MemTable → Segment,查询走稀疏索引 + ZoneMap 裁剪 + 向量化执行。
  2. 三种数据模型:

    • 明细模型(DUPLICATE KEY):原样存储,适合日志流水;
    • 聚合模型(AGGREGATE KEY):按 Key 自动聚合,适合报表预聚合,查询极快;
    • 主键模型(UNIQUE KEY):按主键覆盖更新,适合实时更新和点查,务必开启 Merge-on-Write。

理解了架构和模型,下一篇可以继续讲分区分桶的最佳实践、查询优化和 Compaction 调优。

相关推荐
海绵宝宝转agent1 小时前
Leetcode100+面经
java·笔记·学习·算法·leetcode·深度优先
前进的李工1 小时前
Agent 学习笔记 01:从工具调用到 ReAct 循环
笔记·学习·llm·agent·react·工具调用
海绵宝宝转agent1 小时前
LeetCode100个人题解+java面经总结
java·开发语言·笔记·学习·算法
Riwuarua9 小时前
从近似0基础开始FPGA开发——Part.25:GT 高速收发器架构与 KU5P 资源规划
fpga开发·架构
zhangrelay10 小时前
新版|-|单片机原理与接口技术-2026持续改进优化-从MEGA2560-ESP32与Wokwi到ROS2-Lyrical与具身智能栈
笔记·单片机·嵌入式硬件·学习
Oiiouui11 小时前
Godot-GridGame架构屏幕UI层
架构·游戏引擎·godot
亥时无人机管理系统13 小时前
一网统飞无人机管理系统源码深度评测
架构·私有化部署·无人机·低空经济·无人机管理系统·一网统飞
石逸凡13 小时前
金融产品架构演进之三:极速、一致与高可扩展并存核心目标
金融·架构