本章目标:能讲清 Doris 的 FE/BE 架构、三种数据模型、分区分桶和物化视图,能写建表、upsert、查询 SQL,并比较 Doris、ClickHouse、StarRocks。 前置知识:会写聚合 SQL;建议先读 ClickHouse 入门。
1. 为什么需要它
ClickHouse 单表扫描极快,但复杂多表 join、高并发点查和频繁 upsert 不是它的强项。国内很多公司的实时数仓需要:
- 用 MySQL 协议连接,现有 BI 工具和 JDBC 直接可用;
- 支持多表 join 和高并发查询;
- 支持按主键 upsert,承接 CDC 变更;
- 能实时写入,也能做明细和汇总。
Doris 是这类 MPP OLAP 引擎的代表。它不替代 MySQL 的交易能力,也不一定在所有单表扫描上快过 ClickHouse,选型要看查询模式。
2. 核心概念
2.1 架构:FE 和 BE
客户端(MySQL 9030) → FE(解析/优化/调度,leader/follower/observer)
↓ 分发执行计划
BE(存储 + 计算,多台,数据分片和副本)
- FE(Frontend):管元数据、解析 SQL、生成执行计划、调度查询。生产通常部署 3 个 FE,leader 写元数据,follower 参与选举,observer 只读扩展;元数据存在 FE 的 BDBJE 里,不依赖外部 ZooKeeper。
- BE(Backend):存数据、执行计算、做 compaction。数据按 tablet 分布在多个 BE 上。
- 外部访问:FE 提供 MySQL 协议端口 9030,HTTP 端口 8030(Stream Load 等),BE 也有自己的端口;查询是 MPP 模式,多个 BE 并行执行后由 FE 汇总。
2.2 三种数据模型
| 模型 | 特点 | 适用 |
|---|---|---|
| Duplicate | 保留所有行,不做聚合 | 原始日志、明细、需要保留每一次变更 |
| Aggregate | 相同 key 的 value 列按聚合函数合并 | 预聚合指标、报表 |
| Unique | 相同 key 只保留一行,新数据覆盖旧数据 | CDC 同步、维表、订单最新状态 |
Duplicate 模型用 DUPLICATE KEY(...),不聚合;Aggregate 模型用 AGGREGATE KEY(...),value 列声明 SUM 等聚合方式。下面以 Unique 为例:
CREATE TABLE dwd_order (
order_id VARCHAR(32),
customer_id VARCHAR(32),
order_status VARCHAR(16),
order_amount DECIMAL(18, 2)
) ENGINE = OLAP
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES ("enable_unique_key_merge_on_write" = "true");
注意:
- key 列必须放在最前面,且不能为空;
- Aggregate 模型的 value 列要声明聚合方式(
SUM、MAX、REPLACE等); - Unique 模型默认 merge-on-read,开启 merge-on-write 后查询性能更好,写入成本略高;
- Duplicate 模型不聚合,查明细最快,但数据量最大。
2.3 分区、分桶和副本
表 → 分区 p201808 / p201809
每个分区 → bucket 0..N → tablet → BE1/BE2/BE3 副本
- 分区(Partition):通常按日期范围(
RANGE)或枚举(LIST),用于分区裁剪和生命周期管理;也支持动态分区自动建分区。 - 分桶(Bucket):分区内按 hash 再切分,决定并行度和数据分布;
DISTRIBUTED BY HASH(order_id) BUCKETS 16。 - Tablet:分区 × 分桶的最小数据单元,是副本和 compaction 的基本单位。
- 副本:每个 tablet 默认 3 副本,分布在不同 BE;本地单机练习可设 1,生产不要。
分桶数不是越多越好:tablet 太小会增加元数据和 compaction 压力,太大则并行度不足。常见目标是单个 tablet 几 GB,结合数据量和 BE 数量调整。
2.4 物化视图
Doris 的物化视图(异步 MV)把聚合结果预计算并存储,查询时优化器可以透明改写。它适合固定维度的报表,例如“按品类汇总 GMV”。
CREATE MATERIALIZED VIEW mv_category_gmv
BUILD IMMEDIATE REFRESH COMPLETE ON MANUAL
DISTRIBUTED BY HASH(category) BUCKETS 4
PROPERTIES ("replication_num" = "1")
AS
SELECT category, SUM(gmv) AS total_gmv
FROM dws_trade_minute
GROUP BY category;
REFRESH MATERIALIZED VIEW mv_category_gmv COMPLETE;
REFRESH COMPLETE 全量刷新,适合小结果集;数据量大时可用分区增量刷新。MV 不是“建完自动实时更新”,刷新策略要按业务延迟要求配。
2.5 Doris、ClickHouse、StarRocks 对比
| 维度 | ClickHouse | Doris | StarRocks |
|---|---|---|---|
| 定位 | 单表扫描极快 | MPP、实时数仓、MySQL 生态 | MPP、复杂 join 和高并发 |
| 多表 join | 较弱,需预打宽 | 支持 | 支持,CBO 较成熟 |
| 高并发点查 | 一般 | 较好 | 较好 |
| 实时 upsert | 弱(Replacing 最终一致) | Unique Key 原生支持 | Primary Key 模型 |
| 协议 | 自有协议/HTTP | MySQL 协议 | MySQL 协议 |
| 运维 | 相对复杂 | 中等 | 中等 |
三者都是优秀引擎,选型看团队、查询模式、数据规模和运维能力。
3. 动手示例
Doris 本地部署比单机 ClickHouse 复杂,通常用官方 Docker 快速开始、公司测试集群或云服务。SQL 通过 MySQL 客户端连接 FE:mysql -h 127.0.0.1 -P 9030 -uroot。
建库和分钟汇总表:
CREATE DATABASE IF NOT EXISTS realtime;
USE realtime;
CREATE TABLE dws_trade_minute (
window_start DATETIME, category VARCHAR(32),
order_cnt BIGINT, gmv DECIMAL(18, 2)
) ENGINE = OLAP
UNIQUE KEY(window_start, category)
PARTITION BY RANGE(window_start) (
PARTITION p201808 VALUES <input type="checkbox" class="task sql-task" data-k="sql:lc:196" data-label="('2018-08-01 00:00:00'), ('2018-09-01 00:00:00'))
)
DISTRIBUTED BY HASH(category) BUCKETS 8
PROPERTIES (
"replication_num" = "1",
"enable_unique_key_merge_on_write" = "true"
);
写入和查询:
INSERT INTO dws_trade_minute VALUES
('2018-08-01 10:00:00','health_beauty',12,35988.00),
('2018-08-01 10:00:00','books_general_interest',30,1770.00),
('2018-08-01 10:01:00','health_beauty',8,23992.00);
SELECT category, SUM(order_cnt) AS order_cnt, SUM(gmv) AS gmv
FROM dws_trade_minute
WHERE window_start >= '2018-08-01 10:00:00'
AND window_start < '2018-08-01 11:00:00'
GROUP BY category
ORDER BY gmv DESC;
Unique Key 的 upsert 效果:再插入同一 (window_start, category),旧行会被覆盖,查询不会翻倍。
INSERT INTO dws_trade_minute VALUES ('2018-08-01 10:00:00','health_beauty',13,38987.00);
-- 再查这一行,gmv 已被覆盖为 38987.00,而不是两条相加
物化视图的建表语句见 2.4,建好后手动刷新并查询:
REFRESH MATERIALIZED VIEW mv_category_gmv COMPLETE;
SELECT * FROM mv_category_gmv ORDER BY total_gmv DESC;
生产上用 Stream Load 或 Flink Doris Connector 导入,同一个 label 重复提交不会重复导入,这是导入层的幂等手段。
4. 常见坑
- key 列选错。Unique 模型按 key 覆盖,key 选订单 ID 还是订单 ID + 商品 ID,决定了更新粒度。
- 分区过多。按天分区一年 365 个分区,小数据量时元数据压力大;按数据量选择天/月。
- 分桶随意设。桶太少并行度不够,桶太多小文件多;按 tablet 大小估算。
- Aggregate 模型期待明细更新。它只保留聚合结果,原始明细变化无法直接改;需要明细就用 Duplicate 或 Unique。
- Unique 模型以为查询自动去重。merge-on-read 需要合并,merge-on-write 也要等导入版本生效;理解版本机制。
- MV 建完不刷新。异步 MV 不会自动实时更新,要配刷新策略。
- 单机配置直接上线。生产至少 3 副本,否则 BE 挂了数据不可用;也不要把 Doris 当 MySQL 事务库,它不适合高频单行事务更新。
5. 在线练习
| 题目 | 考点 |
|---|---|
| [196. 删除重复的电子邮箱"> [('2018-08-01 00:00:00'), ('2018-09-01 00:00:00')) | |
| ) | |
| DISTRIBUTED BY HASH(category) BUCKETS 8 | |
| PROPERTIES ( | |
| "replication_num" = "1", | |
| "enable_unique_key_merge_on_write" = "true" | |
| ); |
写入和查询:
```sql
INSERT INTO dws_trade_minute VALUES
('2018-08-01 10:00:00','health_beauty',12,35988.00),
('2018-08-01 10:00:00','books_general_interest',30,1770.00),
('2018-08-01 10:01:00','health_beauty',8,23992.00);
SELECT category, SUM(order_cnt) AS order_cnt, SUM(gmv) AS gmv
FROM dws_trade_minute
WHERE window_start >= '2018-08-01 10:00:00'
AND window_start < '2018-08-01 11:00:00'
GROUP BY category
ORDER BY gmv DESC;
Unique Key 的 upsert 效果:再插入同一 (window_start, category),旧行会被覆盖,查询不会翻倍。
INSERT INTO dws_trade_minute VALUES ('2018-08-01 10:00:00','health_beauty',13,38987.00);
-- 再查这一行,gmv 已被覆盖为 38987.00,而不是两条相加
物化视图的建表语句见 2.4,建好后手动刷新并查询:
REFRESH MATERIALIZED VIEW mv_category_gmv COMPLETE;
SELECT * FROM mv_category_gmv ORDER BY total_gmv DESC;
生产上用 Stream Load 或 Flink Doris Connector 导入,同一个 label 重复提交不会重复导入,这是导入层的幂等手段。
4. 常见坑
- key 列选错。Unique 模型按 key 覆盖,key 选订单 ID 还是订单 ID + 商品 ID,决定了更新粒度。
- 分区过多。按天分区一年 365 个分区,小数据量时元数据压力大;按数据量选择天/月。
- 分桶随意设。桶太少并行度不够,桶太多小文件多;按 tablet 大小估算。
- Aggregate 模型期待明细更新。它只保留聚合结果,原始明细变化无法直接改;需要明细就用 Duplicate 或 Unique。
- Unique 模型以为查询自动去重。merge-on-read 需要合并,merge-on-write 也要等导入版本生效;理解版本机制。
- MV 建完不刷新。异步 MV 不会自动实时更新,要配刷新策略。
- 单机配置直接上线。生产至少 3 副本,否则 BE 挂了数据不可用;也不要把 Doris 当 MySQL 事务库,它不适合高频单行事务更新。
5. 在线练习
| 题目 | 考点 |
|---|---|
| 196. 删除重复的电子邮箱 | 唯一键去重 |
| 176. 第二高的薪水 | 排序取值 |
| 185. 部门工资前三高的所有员工 | 排名函数 |
自测:
- 能根据“是否要保留明细、是否要更新、是否要预聚合”选择 Duplicate、Unique、Aggregate 模型。
- 能解释分区和分桶分别解决什么问题。
- 能说出 Unique Key 模型下 update、delete 的处理方式。
- 能描述 MySQL CDC 同步到 Doris 的关键步骤。
6. 面试延伸
面试题:Doris 的三种数据模型怎么选?
是什么:Duplicate 保留所有行,Aggregate 按 key 预聚合,Unique 按 key 覆盖。
解决什么问题:不同场景对明细保留、写入方式和查询性能要求不同。
我的实操边界:我在本地/测试环境写过三种模型的建表 SQL,理解 key 和 value 列;没有在生产集群做过 compaction 和副本调优。选择标准:要明细用 Duplicate;固定聚合指标用 Aggregate;CDC/最新状态用 Unique。Unique 的 key 粒度决定更新粒度。
面试题:分区和分桶的区别?
是什么:分区按时间/枚举切数据目录,分桶在分区内按 hash 再切 tablet。
解决什么问题:分区做裁剪和生命周期管理,分桶做并行和均匀分布。
我的实操边界:我建过按日期分区、按主键分桶的表,理解 tablet 和副本;没有处理过数据倾斜导致的分桶重设计。分区键通常选日期,分桶键选高基数且常用于 join/过滤的字段;桶数按数据量和 BE 数估算。
面试题:Doris、ClickHouse、StarRocks 怎么选?
是什么:三者都是 OLAP 引擎,ClickHouse 偏单表扫描,Doris 和 StarRocks 偏 MPP 多表和高并发。
解决什么问题:不同查询模式需要不同引擎:日志大宽表、实时 upsert、复杂 join、高并发报表。
我的实操边界:我在本地跑过 ClickHouse 和 Doris 的基础 SQL,StarRocks 只了解定位;没有做过生产选型和压测。选型看查询模式、并发、数据量、MySQL 生态和团队运维。单表日志扫描优先 ClickHouse;实时 upsert 和多表 join 优先 Doris/StarRocks。
7. 本章验收
- 能画出 FE、BE、tablet、副本的关系,并说出 9030 和 8030 端口的作用。
- 能比较 Duplicate、Aggregate、Unique 三种模型,并为三个场景选对模型。
- 能写出按日期分区、按主键分桶的 Doris 建表 SQL。
- 能解释 Unique Key 的 upsert 如何保证重放不翻倍。
- 能创建物化视图并手动刷新,说明它和实时更新的区别。
- 能说出 Doris、ClickHouse、StarRocks 的定位差异,不把“最快”当唯一标准。