一、前言:OLAP 江湖新势力
如果说 ClickHouse 是 OLAP 界的"老炮儿",那 Doris 和 StarRocks 就是正在崛起的"新生代"。
timeline
title OLAP引擎发展史
2008 : Vertica (商业)
2012 : Druid (Apache)
2016 : ClickHouse (Yandex开源)
2017 : Doris (百度Palo开源)
2020 : StarRocks (Doris分支)
2023 : 三足鼎立时代
关系图谱:
graph TB
A[Doris] -->|分支| B[StarRocks]
A -->|原名| C[Palo]
C -->|百度开源| A
A -->|进入| D[Apache孵化]
B -->|独立发展| E[商业化公司]
style A fill:#4ecdc4
style B fill:#ff6b6b
本文核心问题:
Doris/StarRocks 比 ClickHouse 好在哪?又差在哪?
二、架构对比:从根源理解差异
2.1 ClickHouse 架构回顾
graph TB
subgraph "ClickHouse架构"
Client[客户端] --> Node1[CH Node 1]
Client --> Node2[CH Node 2]
Client --> Node3[CH Node 3]
Node1 & Node2 & Node3 --> ZK[(ZooKeeper)]
Node1 --> Disk1[(本地磁盘)]
Node2 --> Disk2[(本地磁盘)]
Node3 --> Disk3[(本地磁盘)]
end
style ZK fill:#ffa502
ClickHouse 特点:
- Shared-Nothing(完全无共享)
- 依赖 ZooKeeper 协调
- 运维复杂度高
2.2 Doris/StarRocks 架构
graph TB
subgraph "Doris/StarRocks架构"
Client[客户端] --> FE1[FE Leader]
Client --> FE2[FE Follower]
Client --> FE3[FE Observer]
FE1 & FE2 & FE3 --> BE1[BE Node 1]
FE1 & FE2 & FE3 --> BE2[BE Node 2]
FE1 & FE2 & FE3 --> BE3[BE Node 3]
BE1 --> Disk1[(本地磁盘)]
BE2 --> Disk2[(本地磁盘)]
BE3 --> Disk3[(本地磁盘)]
end
style FE1 fill:#4ecdc4
style FE2 fill:#45b7d1
style FE3 fill:#96ceb4
核心组件:
| 组件 | 全称 | 职责 |
|---|---|---|
| FE | Frontend | 元数据管理、SQL解析、查询规划 |
| BE | Backend | 数据存储、查询执行 |
2.3 架构差异总结
graph LR
subgraph "ClickHouse"
A1["每个节点既是协调者
也是执行者"] A2["依赖ZK协调"] A3["运维复杂"] end subgraph "Doris/StarRocks" B1["FE负责协调
BE负责执行"] B2["自带元数据管理
无需ZK"] B3["运维简单"] end
也是执行者"] A2["依赖ZK协调"] A3["运维复杂"] end subgraph "Doris/StarRocks" B1["FE负责协调
BE负责执行"] B2["自带元数据管理
无需ZK"] B3["运维简单"] end
三、Doris 深度解析
3.1 Doris 整体架构
graph TB
subgraph "Frontend Layer"
FE[FE集群
MySQL协议兼容] META[(元数据
BDBJE)] end subgraph "Backend Layer" BE1[BE 1] BE2[BE 2] BE3[BE 3] BE4[BE N...] end subgraph "Storage Layer" T1[Tablet 副本1] T2[Tablet 副本2] T3[Tablet 副本3] end FE --> META FE --> BE1 & BE2 & BE3 & BE4 BE1 --> T1 BE2 --> T2 BE3 --> T3
MySQL协议兼容] META[(元数据
BDBJE)] end subgraph "Backend Layer" BE1[BE 1] BE2[BE 2] BE3[BE 3] BE4[BE N...] end subgraph "Storage Layer" T1[Tablet 副本1] T2[Tablet 副本2] T3[Tablet 副本3] end FE --> META FE --> BE1 & BE2 & BE3 & BE4 BE1 --> T1 BE2 --> T2 BE3 --> T3
3.2 数据模型
Doris 支持三种数据模型:
graph TB
A[Doris数据模型] --> B[Duplicate
明细模型] A --> C[Aggregate
聚合模型] A --> D[Unique
主键模型] B --> B1["保留所有原始数据
类似日志表"] C --> C1["预聚合
SUM/MAX/MIN等"] D --> D1["按Key去重
保留最新值"]
明细模型] A --> C[Aggregate
聚合模型] A --> D[Unique
主键模型] B --> B1["保留所有原始数据
类似日志表"] C --> C1["预聚合
SUM/MAX/MIN等"] D --> D1["按Key去重
保留最新值"]
Duplicate 模型(明细模型)
-- 适用场景:日志、行为数据,需要保留所有明细
CREATE TABLE user_behavior (
user_id BIGINT,
event_time DATETIME,
event_type VARCHAR(50),
page_url VARCHAR(500),
duration INT
)
DUPLICATE KEY(user_id, event_time) -- 允许重复
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES ("replication_num" = "3");Aggregate 模型(聚合模型)
-- 适用场景:实时汇总,如PV/UV统计
CREATE TABLE page_view_stats (
page_url VARCHAR(500),
visit_date DATE,
-- 聚合字段
pv BIGINT SUM, -- 自动SUM
uv BIGINT BITMAP_UNION, -- 自动去重
duration BIGINT MAX -- 取最大值
)
AGGREGATE KEY(page_url, visit_date)
DISTRIBUTED BY HASH(page_url) BUCKETS 16
PROPERTIES ("replication_num" = "3");
-- 插入时自动聚合
INSERT INTO page_view_stats VALUES
('/home', '2024-01-15', 1, to_bitmap(1001), 30),
('/home', '2024-01-15', 1, to_bitmap(1002), 45),
('/home', '2024-01-15', 1, to_bitmap(1001), 20); -- 同一用户
-- 查询结果:pv=3, uv=2(1001去重), duration=45Unique 模型(主键模型)
-- 适用场景:需要实时更新的维度表
CREATE TABLE user_profile (
user_id BIGINT,
name VARCHAR(100),
age INT,
city VARCHAR(50),
update_time DATETIME
)
UNIQUE KEY(user_id) -- 按user_id去重
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true" -- 写时合并,实时更新
);3.3 物化视图
flowchart LR
A[Base表
明细数据] --> B[物化视图1
按天汇总] A --> C[物化视图2
按品类汇总] A --> D[物化视图3
按地区汇总] E[智能查询路由] --> B E --> C E --> D F[用户查询] --> E
明细数据] --> B[物化视图1
按天汇总] A --> C[物化视图2
按品类汇总] A --> D[物化视图3
按地区汇总] E[智能查询路由] --> B E --> C E --> D F[用户查询] --> E
-- 基表
CREATE TABLE sales_detail (
order_id BIGINT,
order_date DATE,
product_id BIGINT,
category_id INT,
region VARCHAR(50),
amount DECIMAL(10,2)
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
-- 创建物化视图:按天按品类汇总
CREATE MATERIALIZED VIEW mv_daily_category_sales AS
SELECT
order_date,
category_id,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM sales_detail
GROUP BY order_date, category_id;
-- 查询自动路由到物化视图
SELECT category_id, SUM(amount) -- 自动命中mv
FROM sales_detail
WHERE order_date = '2024-01-15'
GROUP BY category_id;3.4 Doris 2.0 新特性
graph TB
A[Doris 2.0] --> B[倒排索引]
A --> C[Pipeline执行引擎]
A --> D[Merge-on-Write]
A --> E[异步物化视图]
B --> B1["支持全文检索
替代ES部分场景"] C --> C1["查询性能提升
3-10倍"] D --> D1["实时更新
秒级可见"] E --> E1["自动刷新
增量更新"]
替代ES部分场景"] C --> C1["查询性能提升
3-10倍"] D --> D1["实时更新
秒级可见"] E --> E1["自动刷新
增量更新"]
四、StarRocks 深度解析
4.1 StarRocks vs Doris 差异
graph TB
subgraph "共同点"
A1[相同的架构FE/BE]
A2[兼容MySQL协议]
A3[列式存储]
A4[向量化执行]
end
subgraph "StarRocks独有"
B1[CBO优化器更强]
B2[Primary Key表更成熟]
B3[存算分离架构]
B4[更好的JOIN性能]
end
subgraph "Doris独有"
C1[倒排索引全文检索]
C2[更活跃的社区]
C3[Apache基金会背书]
C4[更多国内用户]
end
4.2 StarRocks 查询优化器
flowchart TB
subgraph "CBO Cost-Based Optimizer"
A[SQL语句] --> B[解析器Parser]
B --> C[分析器Analyzer]
C --> D[逻辑优化]
D --> E[物理优化]
E --> F[代价估算]
F --> G[最优计划]
end
subgraph "优化规则"
D --> D1[谓词下推]
D --> D2[列裁剪]
D --> D3[常量折叠]
D --> D4[子查询展开]
E --> E1[Join Reorder]
E --> E2[分布式Join]
E --> E3[Runtime Filter]
end
4.3 存算分离架构(StarRocks 3.0+)
graph TB
subgraph "传统存算一体"
A1[BE节点] --> A2[(本地磁盘)]
A3[BE节点] --> A4[(本地磁盘)]
A5["计算和存储绑定
扩缩容成本高"] end
扩缩容成本高"] end
graph TB
subgraph "存算分离架构"
B1[CN计算节点] --> B3[(对象存储S3/OSS)]
B2[CN计算节点] --> B3
B4["计算独立扩缩
存储无限扩展"] end
存储无限扩展"] end
flowchart LR
subgraph "存算分离优势"
A["弹性计算
按需扩缩CN节点"] --> B["成本降低
热数据本地缓存
冷数据放对象存储"] B --> C["高可用
计算节点无状态
故障快速恢复"] end
按需扩缩CN节点"] --> B["成本降低
热数据本地缓存
冷数据放对象存储"] B --> C["高可用
计算节点无状态
故障快速恢复"] end
4.4 Primary Key 表深度解析
-- StarRocks Primary Key表:支持实时更新
CREATE TABLE orders (
order_id BIGINT,
user_id BIGINT,
status VARCHAR(20),
amount DECIMAL(10,2),
update_time DATETIME
)
PRIMARY KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"enable_persistent_index" = "true" -- 持久化索引,加速更新
);
-- 支持实时UPDATE/DELETE
UPDATE orders SET status = 'COMPLETED' WHERE order_id = 10001;
DELETE FROM orders WHERE order_id = 10002;Primary Key 实现原理:
flowchart TB
subgraph "写入流程"
A[数据写入] --> B{主键是否存在?}
B -->|存在| C[标记旧数据删除]
B -->|不存在| D[直接写入]
C --> E[写入新数据]
D --> E
E --> F[内存索引更新]
end
subgraph "查询流程"
G[查询请求] --> H[内存索引过滤]
H --> I[只返回最新版本]
end
五、实战:构建实时数仓
5.1 典型实时数仓架构
flowchart TB
subgraph "数据源"
A1[MySQL Binlog]
A2[业务日志]
A3[Kafka]
end
subgraph "实时采集"
B1[Flink CDC]
B2[Logstash]
end
subgraph "消息队列"
C[Kafka]
end
subgraph "实时计算"
D[Flink]
end
subgraph "OLAP存储"
E[Doris/StarRocks]
end
subgraph "数据应用"
F1[实时大屏]
F2[BI报表]
F3[即席查询]
end
A1 --> B1 --> C
A2 --> B2 --> C
A3 --> C
C --> D --> E --> F1 & F2 & F3
5.2 Routine Load:从 Kafka 实时入库
-- Doris/StarRocks 支持直接从Kafka消费数据
CREATE ROUTINE LOAD orders_load ON orders
COLUMNS(order_id, user_id, status, amount, update_time),
COLUMNS TERMINATED BY ","
PROPERTIES (
"desired_concurrent_number" = "3",
"max_error_number" = "1000",
"strict_mode" = "false",
"format" = "json",
"jsonpaths" = "[\"$.order_id\",\"$.user_id\",\"$.status\",\"$.amount\",\"$.update_time\"]"
)
FROM KAFKA (
"kafka_broker_list" = "kafka1:9092,kafka2:9092,kafka3:9092",
"kafka_topic" = "orders_topic",
"property.group.id" = "doris_consumer_group",
"property.kafka_default_offsets" = "OFFSET_BEGINNING"
);
-- 查看导入状态
SHOW ROUTINE LOAD FOR orders_load;5.3 Stream Load:批量导入
# 从文件批量导入
curl --location-trusted -u root:password \
-H "Expect:100-continue" \
-H "column_separator:," \
-H "columns: order_id, user_id, status, amount, update_time" \
-T orders_data.csv \
http://fe_host:8030/api/database/orders/_stream_load
# 导入JSON数据
curl --location-trusted -u root:password \
-H "format: json" \
-H "strip_outer_array: true" \
-T orders_data.json \
http://fe_host:8030/api/database/orders/_stream_load5.4 Flink 写入 Doris/StarRocks
// Flink SQL 写入 Doris
CREATE TABLE doris_sink (
order_id BIGINT,
user_id BIGINT,
status STRING,
amount DECIMAL(10,2),
update_time TIMESTAMP(3)
) WITH (
'connector' = 'doris',
'fenodes' = 'fe_host:8030',
'table.identifier' = 'database.orders',
'username' = 'root',
'password' = 'password',
'sink.properties.format' = 'json',
'sink.properties.strip_outer_array' = 'true'
);
INSERT INTO doris_sink
SELECT order_id, user_id, status, amount, update_time
FROM kafka_source;六、高级特性对比
6.1 索引支持
graph TB
subgraph "Doris索引"
A1[前缀索引Prefix]
A2[Bloom Filter]
A3[Bitmap索引]
A4[倒排索引2.0新增]
end
subgraph "StarRocks索引"
B1[前缀索引]
B2[Bloom Filter]
B3[Bitmap索引]
B4[N-Gram索引]
end
subgraph "ClickHouse索引"
C1[稀疏索引Primary]
C2[Bloom Filter]
C3[二级索引Skip]
C4[全文索引有限]
end
6.2 实时更新能力
| 引擎 | 更新方式 | 时效性 | 实现原理 |
|---|---|---|---|
| ClickHouse | CollapsingMergeTree | 最终一致 | 异步Merge |
| Doris (Unique) | Merge-on-Write | 实时 | 写时合并 |
| StarRocks (Primary Key) | Delete+Insert | 实时 | 持久化索引 |
6.3 JOIN 性能
flowchart LR
subgraph "Join类型"
A[Broadcast Join] --> A1["小表广播
适合维表关联"] B[Shuffle Join] --> B1["数据Shuffle
适合大表关联"] C[Colocate Join] --> C1["数据预分布
无需Shuffle"] D[Bucket Shuffle Join] --> D1["部分Shuffle
优化版"] end
适合维表关联"] B[Shuffle Join] --> B1["数据Shuffle
适合大表关联"] C[Colocate Join] --> C1["数据预分布
无需Shuffle"] D[Bucket Shuffle Join] --> D1["部分Shuffle
优化版"] end
-- StarRocks Colocate Join 示例
-- 建表时指定相同的Colocate Group
CREATE TABLE orders (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(10,2)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES ("colocate_with" = "user_group");
CREATE TABLE users (
user_id BIGINT,
name VARCHAR(100)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES ("colocate_with" = "user_group");
-- 相同分桶键的JOIN,数据本地关联,无需Shuffle
SELECT u.name, SUM(o.amount)
FROM orders o JOIN users u ON o.user_id = u.user_id
GROUP BY u.name;七、运维实战
7.1 集群部署架构
graph TB
subgraph "生产环境推荐"
subgraph "FE集群 高可用"
FE1[FE1 Leader]
FE2[FE2 Follower]
FE3[FE3 Follower]
end
subgraph "BE集群 数据节点"
BE1[BE1]
BE2[BE2]
BE3[BE3]
BE4[BE4]
BE5[BE5]
BE6[BE6]
end
LB[负载均衡] --> FE1 & FE2 & FE3
FE1 & FE2 & FE3 --> BE1 & BE2 & BE3 & BE4 & BE5 & BE6
end
资源规划建议:
| 组件 | 最小配置 | 生产配置 | 说明 |
|---|---|---|---|
| FE | 3节点 x 8C16G | 3节点 x 16C32G | 建议SSD |
| BE | 3节点 x 16C64G | 6+节点 x 32C128G | 内存越大越好 |
| 存储 | SSD 500G/节点 | SSD 2T+/节点 | NVME最佳 |
7.2 监控指标
graph TB
subgraph "关键监控指标"
A[FE监控] --> A1["QPS/延迟"]
A --> A2["元数据内存"]
A --> A3["连接数"]
B[BE监控] --> B1["CPU/内存使用率"]
B --> B2["磁盘I/O"]
B --> B3["Compaction进度"]
B --> B4["Tablet数量"]
C[查询监控] --> C1["慢查询数量"]
C --> C2["查询失败率"]
C --> C3["资源队列等待"]
end
-- 查看BE节点状态
SHOW BACKENDS;
-- 查看导入任务
SHOW LOAD;
-- 查看慢查询
SELECT * FROM information_schema.slow_query
WHERE query_time > 10
ORDER BY query_time DESC
LIMIT 20;
-- 查看Tablet状态
SHOW TABLET FROM database.table_name;7.3 常见问题排查
flowchart TD
A[问题排查] --> B{查询慢?}
B -->|是| B1["检查执行计划
EXPLAIN SQL"] B1 --> B2["是否命中分区?"] B2 --> B3["是否有数据倾斜?"] B3 --> B4["Compaction是否积压?"] A --> C{导入失败?} C -->|是| C1["检查错误日志
be.WARNING"] C1 --> C2["内存是否不足?"] C2 --> C3["磁盘是否满了?"] C3 --> C4["数据格式是否正确?"] A --> D{节点不可用?} D -->|是| D1["检查FE/BE进程"] D1 --> D2["检查网络连通性"] D2 --> D3["检查磁盘状态"]
EXPLAIN SQL"] B1 --> B2["是否命中分区?"] B2 --> B3["是否有数据倾斜?"] B3 --> B4["Compaction是否积压?"] A --> C{导入失败?} C -->|是| C1["检查错误日志
be.WARNING"] C1 --> C2["内存是否不足?"] C2 --> C3["磁盘是否满了?"] C3 --> C4["数据格式是否正确?"] A --> D{节点不可用?} D -->|是| D1["检查FE/BE进程"] D1 --> D2["检查网络连通性"] D2 --> D3["检查磁盘状态"]
八、性能调优清单
8.1 建表优化
-- ❌ 不好的建表
CREATE TABLE bad_table (
id BIGINT,
name VARCHAR(500), -- 长度过大
data TEXT, -- 避免TEXT
status VARCHAR(50) -- 应该用枚举优化
)
DISTRIBUTED BY HASH(id) BUCKETS 10; -- 分桶数太少
-- ✅ 好的建表
CREATE TABLE good_table (
id BIGINT,
name VARCHAR(100),
data VARCHAR(5000),
status TINYINT -- 用数字代替字符串
)
DISTRIBUTED BY HASH(id) BUCKETS 64 -- 合理的分桶数
PROPERTIES (
"replication_num" = "3",
"bloom_filter_columns" = "id" -- 常查询字段加布隆过滤
);8.2 查询优化
mindmap
root((查询优化))
分区裁剪
WHERE条件包含分区字段
避免分区函数转换
分桶裁剪
WHERE包含分桶字段
Colocate Join
索引利用
前缀索引命中
Bloom Filter
SQL改写
避免SELECT *
减少子查询
利用物化视图
-- ❌ 不好的查询
SELECT * FROM orders
WHERE DATE(order_time) = '2024-01-15'; -- 函数导致无法裁剪分区
-- ✅ 好的查询
SELECT order_id, amount FROM orders
WHERE order_time >= '2024-01-15 00:00:00'
AND order_time < '2024-01-16 00:00:00';九、选型建议
9.1 场景匹配
graph TD
A{你的场景} --> B{需要实时更新?}
B -->|频繁更新| C[StarRocks Primary Key
或 Doris Unique MOW] B -->|追加为主| D{需要全文检索?} D -->|是| E[Doris 2.0
倒排索引] D -->|否| F{团队技术栈?} F -->|偏好运维简单| G[Doris/StarRocks
无需ZK] F -->|追求极致性能| H[ClickHouse] A --> I{数据量级?} I -->|PB级| J["考虑存算分离
StarRocks 3.0"] I -->|TB级| K[任选都可以]
或 Doris Unique MOW] B -->|追加为主| D{需要全文检索?} D -->|是| E[Doris 2.0
倒排索引] D -->|否| F{团队技术栈?} F -->|偏好运维简单| G[Doris/StarRocks
无需ZK] F -->|追求极致性能| H[ClickHouse] A --> I{数据量级?} I -->|PB级| J["考虑存算分离
StarRocks 3.0"] I -->|TB级| K[任选都可以]
9.2 综合对比表
| 维度 | ClickHouse | Doris | StarRocks |
|---|---|---|---|
| 易用性 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 运维难度 | 高(需ZK) | 低 | 低 |
| 单表性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 多表JOIN | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 实时更新 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| MySQL兼容 | 部分 | 高 | 高 |
| 社区活跃 | 国际化强 | 国内活跃 | 商业化强 |
| 学习成本 | 中等 | 低 | 低 |
9.3 最终建议
flowchart TD
A["如果你是..."] --> B["国内团队
追求稳定运维
预算有限"] A --> C["追求极致分析性能
团队技术能力强"] A --> D["需要实时更新
复杂JOIN场景多"] A --> E["正在用Doris
想要更强性能"] B --> B1["推荐 Apache Doris"] C --> C1["推荐 ClickHouse"] D --> D1["推荐 StarRocks"] E --> E1["评估迁移 StarRocks"] style B1 fill:#4ecdc4 style C1 fill:#ff6b6b style D1 fill:#ffeaa7 style E1 fill:#96ceb4
追求稳定运维
预算有限"] A --> C["追求极致分析性能
团队技术能力强"] A --> D["需要实时更新
复杂JOIN场景多"] A --> E["正在用Doris
想要更强性能"] B --> B1["推荐 Apache Doris"] C --> C1["推荐 ClickHouse"] D --> D1["推荐 StarRocks"] E --> E1["评估迁移 StarRocks"] style B1 fill:#4ecdc4 style C1 fill:#ff6b6b style D1 fill:#ffeaa7 style E1 fill:#96ceb4
十、总结
mindmap
root((Doris/StarRocks))
架构优势
FE/BE分离
无需ZK
运维简单
数据模型
Duplicate明细
Aggregate聚合
Unique/Primary去重
实时能力
Merge-on-Write
实时UPDATE/DELETE
秒级可见
生态集成
Flink Connector
Kafka Routine Load
MySQL协议兼容
一句话总结:
Doris:Apache 基金会背书,社区活跃,国内用户多,2.0 版本支持全文检索
StarRocks:从 Doris 分支而来,CBO 优化器更强,Primary Key 更成熟,存算分离是亮点
两者都是 ClickHouse 之外的优秀选择,尤其适合追求运维简单、需要实时更新、有复杂 JOIN 需求的场景。