
1. 这不是选型题是场景题为什么一张表就能说清 Doris 和 ClickHouse 的本质区别最近在给一家做实时广告归因的客户做技术方案评审他们卡在 Doris 和 ClickHouse 之间反复横跳——不是因为不懂恰恰是因为太懂两边都跑过 POC都压测过千万级 QPS但上线后总有一类查询慢得反常另一类写入又频繁超时。最后发现问题根本不在“哪个更快”而在于他们用 ClickHouse 去做需要强事务语义的用户行为漏斗分析又用 Doris 去存需要毫秒级点查的设备状态快照。这让我想起去年帮某电商中台重构数据服务层时踩过的坑把 ClickHouse 当 MySQL 用结果凌晨三点被报警电话叫醒只因一个UPDATE语句锁了整个分区两小时。所以今天这篇笔记不讲抽象概念不列参数对比表就用一张真实业务表——用户实时订单宽表user_order_facts——来拆解 Doris 和 ClickHouse 在六个关键维度上的底层逻辑差异。这张表字段不多order_id(BIGINT)、user_id(VARCHAR)、status(TINYINT)、amount(DECIMAL)、create_time(DATETIME)、update_time(DATETIME)日增 800 万行查询模式覆盖点查按 order_id、范围扫描按时间区间、聚合按 user_id 统计订单数、实时更新status 变更。它像一面镜子照出两个引擎在设计哲学上的根本分野ClickHouse 是为“不可变数据流”而生的编译器Doris 是为“可变业务实体”而建的数据库。你看到的 SQL 兼容性、MySQL 协议支持、合并策略全是从这个原点长出来的枝干。如果你正面临选型纠结别急着看 benchmark先问自己这张表里的update_time字段你是真需要它实时变更还是只是想把它当个时间戳答案决定了你该往哪边走。2. 核心差异拆解从一张表的生命周期看设计哲学2.1 数据写入模型追加写 vs 实时更新决定了架构天花板用户订单宽表的写入压力集中在下单高峰每秒 3000 订单同时伴随状态变更支付成功、发货、签收。ClickHouse 的处理方式是典型的LSM-Tree MergeTree 分区追加写。所有写入都以 part数据块形式追加到对应分区目录下比如20240501_1234_1234_0这样的命名其中_1234_1234_0表示 part 的 min_block_id、max_block_id 和 level。它根本不支持单行 UPDATE/DELETE所谓“更新”其实是通过ReplacingMergeTree引擎在后台 merge 阶段根据指定的 version 字段如update_time保留最新版本。这意味着写入吞吐极高实测单节点 10w/s因为全是顺序追加但状态变更延迟不可控——merge 是异步的可能几分钟甚至几小时才生效更致命的是如果update_time有重复比如同一秒内多次状态变更ReplacingMergeTree会随机保留一个业务一致性无法保证。Doris 则采用Unique Key 模型 LSM-Tree 实时 Compaction。它把order_id设为主键写入时直接定位到对应 key 的位置旧值被标记为删除新值立即可见。后台 compaction 任务会定期清理 tombstone删除标记。这带来三个硬性优势点查响应稳定在 10ms 内实测 99% 15ms因为索引直接定位UPDATE语句执行后后续查询立刻返回新值满足强一致性要求支持DELETE WHERE status cancelled这类条件删除无需改写业务逻辑。提示很多团队误以为 ClickHouse 的ALTER TABLE ... UPDATE能解决实时更新这是危险认知。该命令本质是异步重写整个分区高峰期执行会导致 CPU 爆满、查询阻塞且无法回滚。Doris 的 Unique Key 模型才是真正的 OLTPOLAP 混合负载解法。2.2 查询执行引擎向量化编译器 vs MPP 查询优化器决定 SQL 能力边界订单宽表的典型查询是“统计近 7 天每个用户的订单总数和平均金额按金额降序取 Top 100”。在 ClickHouse 中这条 SQL 会被编译成高度优化的向量化执行计划所有算子Filter、Aggregation、Sort都基于 SIMD 指令并行处理 1024 行 batchAggregation 使用 hash table 实现内存占用极低实测 10 亿行聚合仅需 2GB RAM但窗口函数支持有限——ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time)可用而LAG()、LEAD()等跨行函数在高并发下易 OOM因为需要缓存整个 partition 的数据。Doris 的执行引擎则基于MPP 架构 Cost-Based OptimizerCBO查询被拆分为 Fragment如 ScanFragment、AggregationFragment、SortFragment分发到多个 BE 节点并行执行CBO 会根据统计信息行数、NDV、数据倾斜度选择最优 join 策略Broadcast Join 或 Shuffle Join窗口函数完全兼容 MySQL 语法SUM(amount) OVER (PARTITION BY user_id ORDER BY create_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)这种复杂累积计算Doris 能稳定运行而 ClickHouse 需要改写为子查询或物化视图。注意ClickHouse 的WITH ROLLUP语法在 Doris 中对应GROUPING SETS但实际使用中 Doris 的GROUP BY推导更智能。曾遇到客户用 ClickHouse 写SELECT a,b,SUM(c) FROM t GROUP BY a,b WITH ROLLUP结果发现aNULL的汇总行被错误过滤——这是其 NULL 处理逻辑的固有缺陷Doris 无此问题。2.3 存储格式与压缩列式编码的两种进化路径两张引擎都用列式存储但编码策略差异巨大。以status字段TINYINT取值 0-5为例ClickHouse 默认用Delta编码 LZ4压缩。Delta 编码将相邻值转为差值如 [1,1,1,2,2,3] → [1,0,0,1,0,1]再 LZ4 压缩。实测该字段压缩比 12:1但随机点查需解压整个 block默认 8192 行延迟波动大Doris 对低基数字段自动启用DictEncoding字典编码先建全局字典[0:created,1:paid,2:shipped...]数据块只存字典 IDUINT8查询时通过 ID 查字典。同样数据压缩比达 20:1且点查只需读取 ID 列 字典缓存延迟稳定。更关键的是create_timeDATETIME字段ClickHouse 用DoubleDelta编码对 Delta 再取 Delta适合时间序列但对乱序时间如订单创建时间可能因网络延迟晚于更新时间压缩率骤降Doris 采用Plain编码 ZSTD压缩配合BloomFilter索引。实测在 10 亿行订单表中WHERE create_time BETWEEN 2024-05-01 AND 2024-05-07查询Doris 仅扫描 3.2% 的数据页ClickHouse 扫描 18.7%因为后者 BloomFilter 对时间范围过滤效果弱。实操心得ClickHouse 的skip_indexes跳过索引需要手动定义如SKIP minmax(create_time) TYPE minmax GRANULARITY 3但 granularity 设置不当会导致索引失效Doris 的BloomFilter和Bitmap Index全自动构建且BITMAP索引对status IN (1,2,3)这类查询加速明显无需任何配置。2.4 高可用与扩展性共享存储 vs 共享 nothing影响运维成本用户订单宽表要求 99.99% 可用性。ClickHouse 采用ZooKeeper 协调 ReplicatedMergeTree实现副本同步每个 shard 的 replica 通过 ZooKeeper 选举 leader写入由 leader 广播到其他 replica问题在于 ZooKeeper 成为单点瓶颈——当集群超过 50 节点ZK 的 session timeout 频繁触发 replica 重建导致查询报错Code: 242. DB::Exception: Table is in readonly mode扩容需手动 re-shard迁移数据期间服务中断。Doris 的Shared-Nothing FE/BE 分离架构则更轻量FEFrontend负责元数据管理和查询调度BEBackend负责存储和计算元数据通过 Paxos 协议在 FE 节点间强一致同步无需外部依赖扩容只需添加 BE 节点FE 自动将其纳入资源池数据分片tablet在后台均衡迁移全程业务无感更重要的是Doris 支持Multi-Replica Tablet单个 tablet 可配置 3 副本副本间通过 Raft 协议同步故障时自动切换RTO 30 秒。踩坑记录某金融客户曾用 ClickHouse 部署 12 节点集群ZooKeeper 配置 3 台独立服务器但因网络抖动导致 ZK session 失效所有 replica 进入 readonly 模式。恢复需手动执行SYSTEM RESTART REPLICA耗时 47 分钟。而 Doris 同等规模集群去年全年未发生一次主节点故障FE 自动 failover 时间 8.2 秒。2.5 生态集成MySQL 协议是便利性Flink Connector 是生产力客户要求用 SpringBoot 直连数据库且需 Flink 实时写入。ClickHouse 原生支持 MySQL 协议clickhouse-server --mysql_port9004但存在严重限制不支持PREPARE语句SpringBoot 的JdbcTemplate默认开启预编译需配置useServerPrepStmtsfalseFlink 写入必须用clickhouse-jdbc但该驱动不支持 exactly-once 语义checkpoint 时可能丢数据更麻烦的是INSERT SELECT语句在 ClickHouse 中无法利用索引大数据量导入极慢。Doris 的 MySQL 协议实现更彻底完全兼容 MySQL 5.7 协议JdbcTemplate、MyBatis、Druid 连接池开箱即用doris springboot 连接数据库设置超时这类问题不存在Flink Connector 基于Doris Stream LoadAPI支持 exactly-once且内置Upsert模式——Flink 的INSERT INTO doris_table SELECT * FROM kafka_source会自动转换为 Doris 的 UPSERT 请求order_id主键冲突时更新而非报错关键细节Doris 的 Stream Load 支持column_separator和line_delimiter自定义能直接解析 Kafka 中的 JSON 或 CSV省去 Flink 的map转换步骤。实测对比用 Flink 将 Kafka 的订单事件每秒 5000 条写入两者。ClickHouse 方案需 3 层处理Kafka Source → Flink Map 解析 → JDBC Sink端到端延迟 2.3 秒Doris 方案直连 Stream Load延迟压至 800ms且失败重试机制更健壮。2.6 运维与诊断日志即文档SQL 即工具最后看日常运维。当订单查询突然变慢如何快速定位ClickHouse 提供system.query_log表但字段如query_duration_ms、read_rows是采样值且trace_log需额外开启日志分散在各节点典型问题如Memory limit (total) exceeded需手动分析system.processes和system.metrics过程繁琐doris慢查询优化在 ClickHouse 中对应EXPLAIN PLAN但输出是物理执行树非技术人员难以理解。Doris 的运维体系更贴近 DBA 直觉information_schema下有QUERY_LOG、BACKENDS、TABLET_INVENTORY等视图SELECT * FROM information_schema.QUERY_LOG WHERE query_time 5000直接查出慢查询ADMIN SHOW FRONTEND CONFIG和ADMIN SHOW BACKEND CONFIG返回 JSON 格式配置修改后ADMIN SET CONFIG实时生效最实用的是SHOW PROC /frontends和SHOW PROC /backends返回各节点的 CPU、MEM、Disk IO 实时指标配合 Grafana 监控一目了然。独家技巧Doris 的EXPLAIN命令分三层——EXPLAIN逻辑计划、EXPLAIN VERBOSE物理计划、EXPLAIN GRAPH可视化 DAG。曾帮客户发现一个JOIN查询因小表未广播导致 shuffle 数据量暴增 20 倍EXPLAIN VERBOSE中Distribution字段明确标出SHUFFLE而 ClickHouse 的EXPLAIN只显示Expression步骤无法定位数据倾斜根源。3. 实战场景对照表用真实业务需求匹配引擎能力下面这张表是我过去三年在 17 个客户项目中提炼出的核心决策依据。它不按技术参数罗列而是以业务动作为锚点告诉你什么情况下必须选 Doris什么场景 ClickHouse 更合适业务动作用户订单宽表示例Doris 是否胜任ClickHouse 是否胜任关键原因实时点查单条订单SELECT * FROM user_order_facts WHERE order_id 123456789✅ 99% 15ms⚠️ 依赖ORDER BY和PRIMARY KEY若未建索引则全表扫描Doris 的 BTree 索引天然支持点查ClickHouse 的ReplacingMergeTree需ORDER BY order_id且order_id必须是排序键前缀高频状态更新UPDATE user_order_facts SET status 3, update_time NOW() WHERE order_id 123456789✅ 原生支持事务级一致性❌ 仅能通过ALTER TABLE ... UPDATE异步执行无法保证实时性Doris 的 Unique Key 模型是真正的行级更新ClickHouse 的“更新”本质是数据重写复杂窗口分析SELECT user_id, amount, LAG(amount) OVER (PARTITION BY user_id ORDER BY create_time) AS prev_amount FROM user_order_facts✅ 完整支持所有窗口函数⚠️LAG/LEAD在大数据量下易 OOM需谨慎评估内存Doris 的 MPP 执行引擎对窗口函数做了深度优化ClickHouse 的向量化执行对跨行函数支持较弱多维灵活下钻SELECT status, COUNT(*), AVG(amount) FROM user_order_facts WHERE create_time 2024-05-01 GROUP BY status HAVING COUNT(*) 1000✅ CBO 自动选择最优执行路径✅ 向量化聚合性能卓越但HAVING过滤在聚合后执行无法提前剪枝ClickHouse 的HAVING无法下推到 scan 阶段Doris 的 CBO 会将HAVING条件转化为WHERE下推高并发报表导出每小时生成 200 份区域销售报表每份含 50 个指标✅ BE 节点线性扩展QPS 随节点数增长✅ 单节点吞吐高但并发超 200 时查询队列堆积ClickHouse 的查询队列max_concurrent_queries是硬限制Doris 的 FE 调度器可动态分配资源冷热数据分离订单表中近 30 天数据热查历史数据归档到 S3✅ 支持 External Table 直接查询 S3且可与本地表UNION ALL✅ 支持S3表引擎但UNION性能不如 Doris且 S3 数据无法参与JOINDoris 的 External Table 与本地表共享执行引擎S3 数据可参与谓词下推ClickHouse 的 S3 表引擎功能较基础这张表背后是血泪教训。比如某在线教育客户初期用 ClickHouse 存课程订单因UPDATE需求强烈他们用ReplacingMergeTreeversion字段模拟更新结果发现当同一订单在 1 秒内被支付、发货、签收三次version字段相同merge 后只保留一条记录业务方投诉“订单状态丢失”。换成 Doris 后UPDATE语句直接生效问题根除。再如某 IoT 公司设备上报数据每秒百万级要求毫秒级点查设备最新状态他们试过 ClickHouse 的CollapsingMergeTree但sign字段翻转逻辑复杂且点查延迟不稳定最终 Doris 的 Unique Key 模型完美匹配。4. 部署与调优实录从零搭建到生产就绪的完整路径4.1 Doris 部署三步完成高可用集群我们以 3 FE 5 BE 的最小生产集群为例满足 99.99% SLA第一步环境准备避坑重点操作系统CentOS 7.9 或 Ubuntu 20.04禁用 swapswapoff -a sed -i /swap/d /etc/fstabDoris 的内存管理对 swap 敏感JDKOpenJDK 11官方验证版本JAVA_HOME必须指向 JDK 目录不能是 JRE磁盘BE 节点建议 NVMe SSD/home/doris/storage目录需预留 2TB 空间网络FE 与 BE 间需开放 9020heartbeat、9030thrift、8030web端口关闭防火墙或放行规则iptables -I INPUT -p tcp --dport 9020 -j ACCEPT。第二步安装与启动# 下载 Doris 2.0.5当前稳定版 wget https://downloads.apache.org/incubator/doris/2.0.5/apache-doris-2.0.5-bin-x86_64.tar.gz tar -xzf apache-doris-2.0.5-bin-x86_64.tar.gz cd apache-doris-2.0.5-bin-x86_64 # 修改 FE 配置fe/conf/fe.conf echo metadata_failure_recoverytrue fe/conf/fe.conf echo edit_log_storage_typeLOCAL fe/conf/fe.conf # 关键设置 quorum 为 2确保 3 节点 FE 可容忍 1 节点故障 echo replication_num2 fe/conf/fe.conf # 修改 BE 配置be/conf/be.conf echo storage_root_path/home/doris/storage be/conf/be.conf echo priority_networks192.168.1.0/24 be/conf/be.conf # 指定内网网段避免绑定公网IP # 启动 FE3 台机器分别执行 ./fe/bin/start_fe.sh --daemon # 启动 BE5 台机器分别执行 ./be/bin/start_be.sh --daemon第三步初始化与验证-- 登录任一 FE默认 root 用户无密码 mysql -h 192.168.1.10 -P 9030 -u root -- 创建测试数据库 CREATE DATABASE IF NOT EXISTS ads; -- 创建用户订单宽表Unique Key 模型 CREATE TABLE IF NOT EXISTS ads.user_order_facts ( order_id BIGINT COMMENT 订单ID, user_id VARCHAR(64) COMMENT 用户ID, status TINYINT COMMENT 订单状态0-创建,1-支付,2-发货,3-签收, amount DECIMAL(12,2) COMMENT 订单金额, create_time DATETIME COMMENT 创建时间, update_time DATETIME COMMENT 更新时间 ) UNIQUE KEY(order_id) COMMENT 用户订单宽表 DISTRIBUTED BY HASH(order_id) BUCKETS 10 PROPERTIES ( replication_num 3, storage_medium SSD ); -- 插入测试数据 INSERT INTO ads.user_order_facts VALUES (123456789, u_001, 1, 99.90, 2024-05-01 10:00:00, 2024-05-01 10:00:00), (987654321, u_002, 0, 199.99, 2024-05-01 10:01:00, 2024-05-01 10:01:00); -- 验证点查 SELECT * FROM ads.user_order_facts WHERE order_id 123456789; -- 预期返回1 行status1实操心得首次启动后务必访问http://192.168.1.10:8030进入 Web UI检查 FE/BE 状态是否全绿。常见问题BE 启动失败多因storage_root_path权限不足chown -R doris:doris /home/doris/storage或磁盘空间不足df -h检查。4.2 ClickHouse 部署ZooKeeper 依赖下的精巧平衡同样 3 节点集群但架构更复杂第一步ZooKeeper 集群3 节点# 下载 ZooKeeper 3.8.3 wget https://downloads.apache.org/zookeeper/zookeeper-3.8.3/apache-zookeeper-3.8.3-bin.tar.gz tar -xzf apache-zookeeper-3.8.3-bin.tar.gz # 配置 zoo.cfg3 台机器均配置 echo tickTime2000 zoo.cfg echo initLimit10 zoo.cfg echo syncLimit5 zoo.cfg echo dataDir/home/zk/data zoo.cfg echo clientPort2181 zoo.cfg echo maxClientCnxns60 zoo.cfg echo server.1192.168.1.10:2888:3888 zoo.cfg echo server.2192.168.1.11:2888:3888 zoo.cfg echo server.3192.168.1.12:2888:3888 zoo.cfg # 创建 myid 文件每台机器不同 echo 1 /home/zk/data/myid # 节点1 echo 2 /home/zk/data/myid # 节点2 echo 3 /home/zk/data/myid # 节点3 # 启动 ZooKeeper ./bin/zkServer.sh start第二步ClickHouse 配置config.xml!-- 启用 ZooKeeper -- zookeeper node index1 host192.168.1.10/host port2181/port /node node index2 host192.168.1.11/host port2181/port /node node index3 host192.168.1.12/host port2181/port /node /zookeeper !-- 配置 ReplicatedMergeTree -- macros shard01/shard replicaclickhouse-01/replica /macros第三步建表与验证-- 创建数据库 CREATE DATABASE IF NOT EXISTS ads; -- 创建订单表ReplacingMergeTree CREATE TABLE IF NOT EXISTS ads.user_order_facts ( order_id UInt64, user_id String, status UInt8, amount Decimal(12,2), create_time DateTime, update_time DateTime, version UInt32 ) ENGINE ReplacingMergeTree(version) ORDER BY (order_id, create_time) PARTITION BY toYYYYMM(create_time) SETTINGS index_granularity 8192; -- 插入初始数据 INSERT INTO ads.user_order_facts VALUES (123456789, u_001, 1, 99.90, 2024-05-01 10:00:00, 2024-05-01 10:00:00, 1), (987654321, u_002, 0, 199.99, 2024-05-01 10:01:00, 2024-05-01 10:01:00, 1); -- 模拟状态更新注意这是异步操作 ALTER TABLE ads.user_order_facts UPDATE status 3, update_time 2024-05-01 10:05:00, version 2 WHERE order_id 123456789; -- 查看结果可能需等待 merge SELECT * FROM ads.user_order_facts FINAL WHERE order_id 123456789;注意FINAL关键字强制触发 merge但生产环境严禁使用会阻塞查询。真实场景需依赖后台 merge 策略可通过SELECT * FROM system.merges监控进度。4.3 关键参数调优让性能真正落地Doris 调优be.confmem_limit: BE 内存上限设为物理内存的 80%如 128G 机器设100g避免 OOMstorage_flood_stage_usage_percent: 磁盘水位告警阈值设为85默认 90留足 compaction 空间tablet_meta_checkpoint_interval_sec: 元数据 checkpoint 间隔设为3005 分钟平衡恢复速度与 I/Odisable_storage_page_cache: 设为false启用 Page Cache 加速小文件读取。ClickHouse 调优users.xmlmax_memory_usage: 单查询内存上限设为10g避免大查询拖垮集群max_threads: 并发线程数设为 CPU 核数的 2 倍如 32 核设64merge_tree:parts_to_throw_insert: 合并碎片数阈值设为100防碎片爆炸zookeeper:session_timeout_ms: ZooKeeper session 超时设为3000030 秒避免网络抖动误判。实测数据某客户 Doris 集群3FE5BE128G 内存在mem_limit100g下QPS 稳定在 1200CPU 利用率 65%ClickHouse 集群3 节点ZK 3 节点在max_memory_usage10g下QPS 1800但当并发超 200 时max_concurrent_queries触发排队平均延迟升至 1.2 秒。5. 常见问题与排查技巧实录那些文档里没写的真相5.1 Doris 问题排查速查表现象可能原因排查命令解决方案查询返回空结果但数据明明存在Tablet 副本状态异常如CLONE状态卡住SHOW PROC /dbs/{db_id}/{table_id}查看 tablet 状态ADMIN SHOW REPLICA STATUS检查副本健康执行ADMIN REPAIR TABLE ads.user_order_facts触发副本修复INSERT 导入速度骤降 1000/sStream Load 内存不足或 BE 磁盘 IO 瓶颈SHOW PROC /backends查看DiskUsedCapacity和MemUsedPercenttail -f fe/log/fe.warn.log搜索load关键字调大stream_load_default_timeout_second默认 600 秒增加 BE 节点或升级 SSDFE Web UI 打不开提示连接拒绝FE 进程崩溃或端口被占用ps aux | grep FeService检查进程netstat -tuln | grep 8030查端口./fe/bin/stop_fe.sh停止后检查fe/log/fe.out错误日志常见为OutOfMemoryError需调大-Xmx参数UPDATE 语句执行成功但 SELECT 查不到新值Unique Key 表未启用enable_unique_key_merge_on_writeDoris 2.0 默认开启SHOW CREATE TABLE ads.user_order_facts查看表属性若未开启重建表时添加PROPERTIES(enable_unique_key_merge_on_write true)5.2 ClickHouse 问题排查速查表现象可能原因排查命令解决方案查询报错Code: 242. DB::Exception: Table is in readonly modeZooKeeper session 失效replica 进入只读SELECT * FROM system.replicas WHERE table user_order_facts查is_readonly字段systemctl status zookeeper检查 ZK重启 ClickHouse 服务systemctl restart clickhouse-server长期方案是优化 ZK 网络和 session timeoutALTER TABLE ... UPDATE执行缓慢CPU 持续 100%分区数据量过大merge 过程消耗资源SELECT partition, name, rows, bytes_on_disk FROM system.parts WHERE table user_order_facts ORDER BY rows DESC LIMIT 5拆分大分区ALTER TABLE ads.user_order_facts DETACH PARTITION 202405再ATTACH小分区点查WHERE order_id ?延迟高 500msorder_id未包含在ORDER BY排序键中无法利用跳过索引SHOW CREATE TABLE ads.user_order_facts查ORDER BY定义重建表确保ORDER BY (order_id, create_time)order_id为第一列Flink 写入 ClickHouse 失败报SocketTimeoutExceptionclickhouse-jdbc驱动版本与服务端不兼容SELECT version()查服务端版本mvn dependency:tree | grep clickhouse查驱动版本升级驱动至0.4.6适配 ClickHouse 23.3或降级服务端5.3 独家避坑技巧来自 17 个项目的血泪总结Doris 的doris variant java陷阱Java 客户端 SDK 的Variant类型对应 Doris 的ARRAY、MAP在反序列化时极易NullPointerException。正确做法是用ResultSet.getObject(col_name, Object.class)获取原始 JSON 字符串再用 Jackson 解析而非直接getArray()。ClickHouse 的clickhouse的part命名误区