最近被问得最多的一个问题,不是某张报表怎么写,而是:Doris 还是 ClickHouse?做实时数仓、写 BI 报表、搭用户行为分析,大家一上手就会撞上这两个名字。它们都是当前 OLAP 领域最热门的列式分析引擎,都能扛几亿行甚至几十亿行数据的聚合查询,但真到了落地阶段,从建表、导数据、写查询到日常运维,体感差别非常大。
这篇是我的实战对比笔记,不是官方文档翻译,重点放在六个最影响决策的差异上:架构模型、数据组织、查询能力、写入与更新、一致性、运维与生态。我分别用社区版 Docker 和二进制方式部署了两套环境,造了模拟业务数据,跑了典型的报表查询和写入压测,也踩了不少坑。适合正在做技术选型,或者已经用了其中某一个、正犹豫要不要迁移的团队参考。
1. 先上结论:一张表看清六个关键差异
六个维度的差异,先给一张表:
| 对比维度 | Doris | ClickHouse |
|---|---|---|
| 架构模型 | MPP 架构,FE 负责元数据与 SQL 规划,BE 负责存储与计算 | Shared-Nothing 列存,MergeTree 家族引擎,单节点能力强 |
| 数据组织 | 表按 tablet 分片,多副本强一致管理 | 分区 + part + 稀疏索引,每次写入生成新 part,后台异步 merge |
| 查询能力 | 复杂多表 Join 更稳,高并发点查能力更强 | 单表聚合/扫描极快,复杂 Join 易吃内存,高并发是短板 |
| 写入与更新 | 支持高频小批量导入,唯一键模型可直接更新删除 | 适合大吞吐批量写入,更新删除属于重操作,mutation 成本高 |
| 数据一致性 | 多副本强一致,写成功即可读到一致数据 | 默认异步复制,有同步窗口,要求强一致需要额外配置 |
| 部署与生态 | 组件多,部署稍重,兼容 MySQL 协议与工具生态 | 部署轻,有原生 HTTP 接口,周边工具自成一套 |
选这六个维度,不是因为官方文档只写了这些,而是因为这些点几乎是所有真实选型中绕不开的决策项。团队做 OLAP 选型,第一步看架构能不能匹配业务规模,第二步看数据明细怎么组织、怎么保证能查得快,第三步看写入链路能不能跟上实时性要求,第四步看多表和并发查询是不是常态,第五步看团队能不能维护得住这套系统,最后再看它跟现有技术栈好不好对接。这六项全过一遍,选型基本就落地了。
下面我逐个展开,重点讲清楚“为什么会这样”和“实际用起来是什么感受”。
2. 差异的根子在基因:架构设计与数据分片的底层逻辑
2.1 Doris 为什么能扛复杂查询:FE/BE 分工与 tablet 分片
Doris 整体是 MPP 架构,核心角色分两类:FE 和 BE。FE 是前端节点,负责元数据管理、SQL 解析、生成执行计划、查询调度;BE 是后端节点,负责数据存储和实际计算。客户端连的是 FE,FE 把一条 SQL 拆成可以在多个 BE 上并行执行的子计划,最后汇总结果。
这个架构最值钱的地方有两个。第一,执行计划是全局调度的,多表 Join 可以在不同 BE 上并行做,不依赖某一条 SQL 自己碰巧写得适合分布式。第二,数据按 tablet 分片,每个 tablet 有多个副本,副本之间通过一致性协议同步,某个 BE 挂了,查询自动切换到其他副本,对业务透明。
tablet 的概念可以简单理解成表数据的横向切片。创建表时可以指定分桶列和分桶数,Doris 2.x 之后支持 auto bucket,它会根据数据量和分区大小自动调整分桶。分桶粒度直接影响查询并发度,实践中见过很多人贪图省事用默认配置,结果一张大表只有几个 tablet,查询根本起不来并行。反过来,tablet 数量太多,FE 的元数据管理压力也会变大。
我的建议是:如果表会按时间分区,每个分区的数据量控制在几百万到几千万行级别,分桶数按单分区数据量设成 8 到 32 个,别拍脑袋设一个固定值不管了。数据涨上去之后,Doris 做分桶调整比 ClickHouse 重分布数据要友好得多,但最好还是一开始就把模型定好。
2.2 ClickHouse 的能力来源:MergeTree、分区和分布式表
ClickHouse 的核心不是分布式能力,而是本地单机的 MergeTree 家族引擎。它用列式存储、稀疏主键索引、向量化执行,把单节点的扫描和聚合速度做到了极致。
数据写入时,ClickHouse 会把数据按分区拆成一个或几个不可变的 data part,落盘后由后台线程异步合并成更大的 part。查询时,它借助分区裁剪跳过无关分区,再借助主键的稀疏索引(granule 级别的 min/max 信息)跳过无关数据块。
“分布式表”在 ClickHouse 里是一个逻辑层,真正存储数据的是各个节点上的本地表。分布式表只是把查询分发到不同分片、汇总结果的壳。这一点和 Doris 的全局调度有本质区别:Doris 的 FE 知道每个 tablet 在哪、数据长什么样,可以做出全局最优的执行计划;ClickHouse 的分布式查询更像把任务撒下去、各自算完再拼起来。
这个设计带来的一个经典现象是:ClickHouse 集群性能非常依赖分片键和分区键选得好不好。分片键选得均匀,每个节点负载均衡;选得不好,数据倾斜和热点几乎是必然的。
2.3 架构差异带来的真实体感
架构差异不会直接写在性能报告里,但运维时感受特别明显。
扩容这件事,Doris 加一个 BE 节点后,tablet 会自动在集群里做均衡,旧数据不用动,新数据写入时 FE 也会尽量往新节点分配。ClickHouse 加新节点后,旧数据不会自动搬过去,常见的做法是把数据重新导出再导入,或者用分布式表 + 后台任务慢慢搬,过程比 Doris 重得多。
故障恢复也是。Doris 副本切换是内部的,查询层无感知;ClickHouse 如果某个分片只剩一份数据且节点挂了,查询要么报错要么查到不完整数据,除非你在每个分片都配置了多副本并且把 internal_replication 打开。理解了这一层,才能理解为什么很多人用 ClickHouse 是“单机很强,集群要精心伺候”。
3. ClickHouse 的 part 命名机制:初看是细节,细看是存储哲学
这个话题是我翻了无数 ClickHouse 排错帖之后觉得最值得单独讲的一块。大多数人第一次在数据目录里看到202405_1_3_0这种文件夹,都不知道它是干嘛的,但理解了 part 命名,你就理解了 ClickHouse 的写入、合并、变更机制。
3.1 part 命名里到底藏了什么信息
在默认数据目录/var/lib/clickhouse/data/default/表名/下,每一张表会对应一个目录,里面每个人可见的文件夹就是一个 data part。典型的命名像这样:
202405_1_3_0 202405_4_9_1 202406_1_1_0拆开来看,part 命名通常由这几段组成:
- 第一段是分区 ID。比如
202405表示这个 part 属于 2024 年 5 月这个分区。分区 ID 不是必须和原始分区字段值长得一样,它是由分区表达式计算出来的。 - 第二段是
min_block_num,最小块号。 - 第三段是
max_block_num,最大块号。 - 第四段是
merge_level,合并层级。 - 某些新版本如果发生过 mutation,命名里还会带额外的哈希后缀,表示这个 part 被重写过。
块号是 ClickHouse 全局递增分配的,每写入一批数据就会拿到一个新的 block number 范围。所以202405_1_3_0表示:202405 分区下,包含块号 1 到 3 的一个 part,合并层级是 0,即它刚写入没多久,还没被合并过。
通过这个命名,你只需要一条命令就能快速判断表的数据健康度:
ls /var/lib/clickhouse/data/default/your_table/ | head -20如果看到大量过程是_0结尾的小 part,说明这张表正在被高频小批量写入,或者 merge 速度跟不上写入速度。
3.2 part 太多为什么会拖垮查询
每个 part 都自带独立的索引和标记文件。查询时 ClickHouse 需要在内存里把所有相关 part 的索引信息合并起来,扫描时也要逐个 part 读数据块,最后再把结果合并。part 数量越多,文件句柄占用越多,索引合并开销越大,查询延迟肉眼可见地变差。
更麻烦的是,后台 merge 线程会一直尝试把小 part 合并成大 part,part 太多意味着 merge 压力一直很大,磁盘 IO 和 CPU 被 merge 吃掉了,真正给查询的资源就少了。这就是为什么很多人明明配了很多 CPU,压测时却发现查询和 merge 在抢资源。
3.3 预防和治理的实操
- 首选方案是控制写入批次。ClickHouse 适合大 batch 写入,如果业务侧只能产生小批次数据,中间加一层 Buffer 表或 Kafka 缓冲,把写入聚合成几十 MB 一个 batch 再落盘。
- 分区粒度要合理。某些场景下分区太细,每个分区都只有一点点数据,也会产生大量小 part。日志表按天分区通常够了,没必要按小时。
- 监控 system.parts 表。重点看 active 字段等于 1 的行数、bytes 大小、level 层级:
SELECT table, partition, count() AS total_parts, sum(rows) AS total_rows, sum(bytes) AS total_bytes, max(level) AS max_level FROM system.parts WHERE active AND table = 'your_table' GROUP BY table, partition ORDER BY total_parts DESC;如果某个分区的 active part 数持续超过几百甚至上千,就需要考虑强制合并或调整写入模型。
- 偶尔手工
OPTIMIZE TABLE your_table FINAL可以强制合并,但不建议频繁使用。FINAL 会做全量合并,扫描和重写的数据量极大,生产环境跑一次可能会把磁盘 IO 打满。把它当成治理手段,而不是日常运维动作。
我用这套方法排查过几次 ClickHouse 查询越来越慢的问题,最后根因基本都是小 part 堆积。把写入聚合之后,查询速度自己就回来了。
4. 查询体验的分水岭:多表关联和高并发谁更扛得住
如果只看单表聚合,ClickHouse 的扫描速度确实惊艳,一条 count + group by 跑几亿行也就一两秒。但真实业务很少只有单表查询,一旦把多表 Join 和高并发这两个条件加进来,局面就不一样了。
4.1 Doris 的多表 Join 为什么更省心
Doris 2.x 引入 Nereids 优化器之后,Join 重写、Join Reorder、谓词下推、Colocate Join、Bucket Shuffle Join 这些能力都逐步成熟了。对常见的星型模型,几张百万到亿级的表做关联,只要内存足够,优化器基本能选出一个合理的执行形态。
尤其值得说的是 Colocate Join。如果两张大表的分桶列和分桶数一致,Doris 能让 Join 在本地完成,数据不用跨节点传输,这在常规 MPP 引擎里属于杀手级能力。建表时把 Join 键设成相同的分桶列,后面的查询收益非常大。
Doris 对高并发点查也做了不少优化,比如 short circuit point query、缓存、倒排索引等。几十并发以内的精确查询体验很接近在线服务。我们压测过用主键查单行,几百 QPS 都很稳。
4.2 ClickHouse 的 Join 为什么让人又爱又恨
ClickHouse 跑单表聚合是强项,多表 Join 能跑,但心智负担比较重。默认的 hash join 会把右表构建到内存里,大表 Join 大表很容易内存爆掉。很多团队不得不用 Global Join 把所有分片的数据广播到查询节点,内存和网络压力都上去了。
社区惯用的做法是用字典表、子查询、预聚合、bitmap 去重等方案绕开 Join。比如把维度表做成 Dictionary,查询时直接取字典值,而不是真去 Join。这确实能解决问题,但意味着 SQL 写起来要顾虑很多,不像 Doris 那样你可以按标准 SQL 的直觉去写,剩下交给优化器。
新版 ClickHouse 的 Join 能力一直在进步,比如支持更丰富的 join 算法、并行 hash join,但根本上它还是把“单表扫描极快”这个优势放在第一位的引擎。如果你的业务一半以上的查询要跨多张表,ClickHouse 的学习和调优成本要明显高于 Doris。
4.3 高并发:一个容易翻车的盲区
ClickHouse 的单次查询调度链路很重。每次查询都要解析 SQL、构建 pipeline、把任务分发到线程池,查询越短,单次开销占比越高。简单查询压测到几百 QPS 就可能把单核 CPU 打满,再往上走,延迟会快速劣化。
这不是 ClickHouse 的缺陷,而是设计取舍。它默认使用者不是拿它当在线服务,而是拿它做分析查询。但如果你真的要用它支撑报表 API,常见解法是加一层查询缓存、或者用 Materialized View 把结果预先算好。还有不少人会同时用多个只读副本分摊查询压力。
Doris 因为 FE 层做了很多查询规划和缓存,加上 BE 的并行调度能力强,在高并发点查场景下的表现要好很多。这也是为什么我看到越来越多人把 Doris 用在用户标签查询、订单实时查询这类“分析库干在线活”的场景。
用一句话总结我实测下来的体感:如果你要做的是多表关联复杂分析,Doris 会省心很多;如果核心场景就是超大单表按条件做聚合,ClickHouse 的原始性能优势不容忽视。
5. 写入链路与更新能力:实时数仓最容易翻车的环节
选型时大家关注查询多,但真正上线后翻车的往往在写入和更新。这一节值得仔细看。
5.1 Doris 实时写入与主键更新
Doris 从设计上就考虑了实时写入。Stream Load 可以把 CSV、JSON 格式的数据通过 HTTP 方式批量导入,Routine Load 可以持续消费 Kafka 数据,配合 Flink CDC 做数据库实时同步也已经是很成熟的方案。高频小批量导入对 Doris 来说没有太大压力,因为它的存储层是支持随机写的,不是靠文件合并来摊平写入代价。
更重要的一点是 Doris 的唯一键模型支持真正的更新和删除。应用层发一条 DELETE 或者 UPDATE,底层走 MVCC,数据可以被实时修正。这对做实时数仓太重要了。比如订单表的状态字段从“待支付”变成“已支付”,业务希望报表立刻反映出来,Doris 可以直接更新那一行,查询看到的就是最新值。
当然,这不意味着你可以把 Doris 当成 MySQL 用。它本质还是 OLAP 引擎,超高并发、每秒几万次的单条随机更新依旧不适合。
5.2 ClickHouse 的写入哲学和 UPDATE/DELETE 的代价
ClickHouse 的写入哲学是“尽量一次性写入大批量数据”。一次 INSERT 产生一个或多个新 part,写入的数据量越大、批次越少,整体效率越高。反过来,如果每秒都来几十个小 INSERT,part 会迅速膨胀,后续查询变慢,merge 也会把磁盘 IO 吃光。
更新删除在 ClickHouse 里不是不能做,但属于重操作。ALTER TABLE ... UPDATE/DELETE本质是一次 mutation,ClickHouse 会把涉及分区的数据整个重写一遍。几亿行的表做一次更新,可能要跑几分钟甚至更久,期间 IO 压力很大。
所以社区更推荐的模式是“不改数据,用语义去重”。ReplacingMergeTree 通过版本号或时间戳在查询时保留最新记录,CollapsingMergeTree 用正负折叠的方式处理事实表修正,AggregatingMergeTree 在合并时预聚合。这些模型都需要业务在写入 Schema 上提前设计,而不是事后像传统数据库那样直接改。
5.3 一致性窗口:强一致和最终一致的取舍
Doris 的多副本是强一致设计。数据写入成功后,任意副本上都能读到最新数据,不需要考虑复制延迟。
ClickHouse 的多副本默认是异步复制。数据先写到某一个副本,再由后台线程复制到其他副本,中间存在一个同步窗口。窗口大小取决于数据量和网络,正常情况下是秒级,压力大时可能更长。如果是单副本,那就不存在同步问题,但节点挂了数据就没了。
如果你的下游业务能接受最终一致,ClickHouse 没问题。但如果要做订单、余额、库存这类对一致性敏感的分析,必须考虑清楚这个窗口带来的影响,或者干脆选强一致引擎。
6. 运维体感与周边生态:部署、连接超时、半结构化与慢查询
6.1 部署难度:一个轻一个重
部署体感上,ClickHouse 明显更轻。在 Ubuntu 上安装就是几条命令的事,装好之后改一下配置文件,启动服务就能查数据。单机跑起来很快,开发环境十分钟搞定。
Doris 相对重一些。至少要规划 FE 和 BE 两类进程,生产环境一般建议 3 个 FE 组成高可用,BE 节点数量和存储盘位也要事先想好。FE 的元数据目录要放到独立盘,BE 的存储盘要规划多盘配置。装好之后需要用 MySQL 客户端连上 FE,执行ALTER SYSTEM ADD BACKEND把 BE 节点注册进来。
我的感受是:如果只是想快速验证,ClickHouse 先跑起来容易;如果要搭一个能长期稳定服务的生产集群,两者都需要认真规划,只是 ClickHouse 的配置文件更集中,Doris 需要照顾的进程和状态更多。
6.2 连接与超时:Java/Spring Boot 常见坑
Doris 兼容 MySQL 协议,所以 Java 项目可以直接用 MySQL JDBC 驱动连接。但这个兼容性也会带来一个经典坑:长查询超时。
很多团队用 Spring Boot 连 Doris,连接串还是按连 MySQL 的习惯配置,结果发现一个跑几十秒的 SQL 经常被中途断开。原因通常是 socketTimeout 配置不规范,或者连接池里的连接被数据库空闲超时策略杀掉后没有及时重建。
我建议按这个思路配置:
jdbc:mysql://fe_host:9030/db?connectTimeout=10000&socketTimeout=600000&rewriteBatchedStatements=true&useSSL=false同时把 HikariCP 的connection-timeout设成 5 秒左右,max-lifetime不要超过数据库端的空闲踢出时间。Doris 的 FE 有一个 session 变量max_execution_time,单位毫秒,默认一般是 300000(5 分钟),如果业务里确实有跑很久的查询,这个值也要相应调大。这几个参数配合好,连接层问题基本能避免。
ClickHouse 这边反而简单一些。官方提供 HTTP 接口(默认 8123)和原生 TCP 接口,Java 里有 clickhouse-jdbc 和 clickhouse-client 两种选择。HTTP 接口对开发特别友好,直接发 POST 请求带上 SQL 就能查询,也很容易封装成各种服务的上报接口。
6.3 半结构化数据处理:variant 与 JSON 函数的对比
日志、埋点这类数据通常都是半结构化 JSON,字段经常变化。这个场景下两边的处理逻辑差异很大。
Doris 2.1 开始支持 variant 类型,可以像存 JSON 一样把一个不确定 Schema 的字段直接塞进去。它会在内部自动识别字段并建立列存结构,还能对 variant 里的字段建倒排索引,查询时用variantCol.fieldName的方式访问。Java 客户端拿到 variant 字段时,走 MySQL 协议返回的是 JSON 字符串,用 Jackson 或 Fastjson 再解析成对象就行,整体体验很顺。
ClickHouse 的半结构化方案相对传统一些。最常用的方式是用 String 类型把整个 JSON 存下来,查询时用JSONExtractString、JSONExtractInt这些函数动态解析。新版支持了 JSON 类型,可以自动识别部分字段,但成熟度和索引能力还在演进中。
如果你的数据是日志、埋点、爬虫结果这种字段经常变化的,Doris 的 variant 会明显省事。你要做的只是建表时多声明一个 variant 列,后续新字段不用改表结构。
6.4 慢查询排查的基本手法对比
Doris 慢查询排查,我的习惯是先开审计日志和查询 Profile。设置is_report_success = true之后,FE 会记录每个查询的详细 Profile,能看到每台 BE 上 scan 了多少行、join 是否有数据倾斜、内存和耗时都花在哪个算子上了。配合 EXPLAIN 看执行计划,基本能定位问题。常见的慢查询原因有:统计信息没收集(该跑 ANALYZE TABLE 没跑)、谓词下推失败、tablet 分布不均导致个别 BE 成为热点、内存超限触发落盘。
ClickHouse 的排查路径是另一套。system.query_log表记录了每条查询的耗时、读取行数、内存峰值,直接查它就能找到最慢的查询列表。再用system.parts看 part 数量是否异常,用system.processes看正在跑的查询,必要时用 EXPLAIN PIPELINE 看执行计划的并行度。常见慢查询原因有:part 数过多、没有走分区裁剪、大字段读取过多、GROUP BY 时数据倾斜。
两边的排查思路都有章可循,但工具差异很大。Doris 更像传统数据库的体验,Profile 细致,适合 DBA 思维;ClickHouse 则更依赖系统表,熟悉之后也非常高效。
7. 最后的选择题:你的场景更适合哪一个
7.1 按场景直接给结论
根据这段时间的实战体验,我自己的选型判断标准是这样:
选 Doris 的场景:
- 业务对数据更新和删除有实时要求,不能接受“查不到最新值”。
- 复杂多表 Join 是常态,不想花大量精力优化 SQL 去绕开 Join。
- 需要支撑较高并发的查询 API,比如用户标签、订单详情这类点查。
- 数据中包含大量 JSON/半结构化字段,希望按字段直接查询。
- 团队更熟悉 MySQL 生态,想用 SQL 和 MySQL 客户端一把梭。
选 ClickHouse 的场景:
- 核心场景是超大单表的聚合分析,比如日志、事件流、监控指标。
- 写入以批量导入为主,数据基本不可变,或者能接受最终一致。
- 查询并发不高,但单条 SQL 的数据扫描量极大,需要极致吞吐。
- 团队愿意接受一套独立的 ClickHouse 运维体系,包括 part、分区键、分片键这些概念。
7.2 也可以不二选一
很多团队最终不是二选一,而是双跑。Doris 负责实时写入、更新、高并发报表查询,ClickHouse 负责日志明细的海量沉淀和 ad-hoc 分析。中间用管道做数据同步,各取所长。
这种方式初期会多一些运维成本,但对业务侧体验确实最好。尤其是数仓链路已经比较成熟的情况下,没有必要为了统一而强行把其中一个塞进所有场景。
7.3 我个人实操后的最后建议
如果让我重新做一次选型,我不会再先问社区“谁更快”,而是先列出三个高频查询和两个写入链路,在两套环境上各压一遍。实测数据永远比任何人的经验都更贴近你的业务。
另外有一个小提醒:无论选哪个,都要把监控体系提前搭好。Doris 重点盯 FE 元数据和 BE 的磁盘、内存、tablet 分布;ClickHouse 重点盯 part 数量、merge 队列、副本同步延迟。这两个系统在监控上各有各的脾气,提前把 system 表指标接到告警里,后面能省掉无数个从睡梦中被叫醒的夜晚。