OLAP引擎选型终极对比ClickHouse vs Doris vs StarRocks的性能基准OLAP引擎选型是数据架构中最具挑战性的决策之一。ClickHouse、Apache Doris和StarRocks三者在性能基准上的差异直接影响着后续数年的架构方向。本文基于一个完整的选型项目提供三引擎的真实Benchmark数据对比和场景化决策指南。一、当SSB基准测试的数字带偏了整个选型方向去年做过一次OLAP引擎选型启动阶段就被SSBStar Schema Benchmark测试结果所困扰。ClickHouse在单表聚合上压倒性领先Doris在多表JOIN上表现更好StarRocks则在全场景上最均衡。最初倾向选ClickHouse因为单表场景占业务80%。但在真实业务的POC测试中一个反直觉的结果出现了ClickHouse在含有5个维度表JOIN的查询中表现严重退化而Doris和StarRocks因为有更完善的CBO优化器和物化视图自动改写反而在复杂查询上胜出。这说明Benchmark的选择比Benchmark的结果更重要。SSB测试使用的是标准的星型模型数据分布均匀没有数据倾斜。但真实业务中数据倾斜是常态——比如电商场景中头部商家的订单量可能是长尾商家的1000倍。ClickHouse在处理倾斜数据时单表聚合的性能依然出色向量化执行不受数据分布影响但在JOIN场景下倾斜的JOIN键会导致某个Shard上的数据量远超其他节点拖慢整体查询。而StarRocks的Runtime Filter和Doris的Colocate JOIN机制都能有效缓解数据倾斜对JOIN的影响。更深层的问题是SSB的查询模式过于教科书化——固定的星型模型、固定的聚合维度。而真实BI场景中查询模式是多变的用户可能随时切换分析维度、增加过滤条件、嵌套子查询。ClickHouse在这种即席查询场景下因为缺少CBO基于成本的优化器严重依赖人工优化手动设置JOIN顺序、手动创建物化视图运维负担很重。二、三大OLAP引擎的架构差异架构图揭示了三者的核心技术差异。ClickHouse的核心优势是向量化执行引擎——它按列批量处理数据充分利用CPU SIMD指令和Cache Locality在单表聚合场景下的性能通常是另外两者的3-5倍。但ClickHouse没有CBO优化器JOIN顺序由SQL书写顺序决定或通过手动Hint调整这意味着复杂查询的性能高度依赖开发者的经验。Doris和StarRocks后者是Doris的分支演进都具备完整的CBO优化器能自动选择最优的JOIN顺序、JOIN策略Hash JOIN vs Broadcast JOIN和聚合方式。StarRocks相比Doris的额外优势在于物化视图的自动改写——它可以自动识别查询模式将查询路由到预聚合的物化视图上而不需要用户手动指定。在真实业务SQL上的测试更能说明问题。以下是一个电商分析场景的典型查询-- 多维分析查询订单按地区、品类、时间段聚合 SELECT r.region_name, p.category, toStartOfMonth(o.created_at) AS month, count(*) AS order_cnt, sum(o.amount) AS total_amount, avg(o.amount) AS avg_amount FROM orders o JOIN users u ON o.user_id u.id JOIN regions r ON u.region_id r.id JOIN products p ON o.product_id p.id WHERE o.created_at 2025-01-01 AND o.status completed GROUP BY r.region_name, p.category, month ORDER BY total_amount DESC LIMIT 100; -- 执行计划对比5亿行事实表 3个维度表: -- ClickHouse: 顺序JOIN, 无CBO重排 -- - 5亿行扫描 - Hash JOIN users - Hash JOIN regions - Hash JOIN products -- - 聚合 - 排序 - Limit -- 实际执行: 8.2秒 (数据倾斜导致单节点瓶颈) -- -- StarRocks: CBO优化 Runtime Filter 物化视图命中 -- - 物化视图命中 (预聚合月度数据) - 仅扫描5000行 -- 实际执行: 0.3秒 (物化视图改写) -- -- Doris: CBO优化 Colocate JOIN -- - Colocate JOIN(users, regions) 同分布 - Broadcast JOIN products -- - 聚合 - 排序 - Limit -- 实际执行: 1.8秒这个案例生动说明了CBO和物化视图的价值。ClickHouse的8.2秒并非因为向量化执行不够快而是因为没有CBO来优化JOIN顺序也没有自动物化视图来短路查询。StarRocks的0.3秒是因为CBO识别到存在预聚合的物化视图将原始查询自动改写为对物化视图的查询——这种优化对用户完全透明。三、性能基准测试工具#!/usr/bin/env python3 OLAP引擎性能基准测试工具 import time import statistics from typing import Dict, List, Callable, Optional from dataclasses import dataclass, field import json dataclass class QueryBenchmark: name: str category: str # single_table, multi_join, aggregation, window sql: str expected_rows: Optional[int] None dataclass class BenchmarkResult: benchmark: QueryBenchmark execution_times: List[float] field(default_factorylist) errors: List[str] field(default_factorylist) property def avg_time(self) - float: if not self.execution_times: return float(inf) return statistics.mean(self.execution_times) property def p50_time(self) - float: if not self.execution_times: return float(inf) sorted_times sorted(self.execution_times) idx len(sorted_times) // 2 return sorted_times[idx] property def p99_time(self) - float: if not self.execution_times: return float(inf) sorted_times sorted(self.execution_times) idx int(len(sorted_times) * 0.99) return sorted_times[min(idx, len(sorted_times) - 1)] property def success_rate(self) - float: total len(self.execution_times) len(self.errors) if total 0: return 0 return len(self.execution_times) / total class OLAPBenchmarkRunner: def __init__(self, engine_name: str, engine_config: dict): self.engine_name engine_name self.engine_config engine_config self.benchmarks: List[QueryBenchmark] [] self._init_benchmarks() def _init_benchmarks(self): 初始化标准基准测试集 self.benchmarks [ QueryBenchmark( 单表全量聚合, single_table, SELECT region, count(*), sum(amount) FROM orders GROUP BY region, ), QueryBenchmark( 单表时间窗口聚合, single_table, SELECT toStartOfHour(created_at) as h, count(*), avg(amount) FROM orders WHERE created_at now() - INTERVAL 30 DAY GROUP BY h ORDER BY h, ), QueryBenchmark( 两表JOIN聚合, multi_join, SELECT u.region, count(*), sum(o.amount) FROM orders o JOIN users u ON o.user_id u.id GROUP BY u.region, ), QueryBenchmark( 五表星型JOIN, multi_join, SELECT d.year, p.category, c.region, sum(f.amount) FROM fact_orders f JOIN dim_date d ON f.date_id d.id JOIN dim_product p ON f.product_id p.id JOIN dim_customer c ON f.customer_id c.id JOIN dim_store s ON f.store_id s.id WHERE d.year 2026 GROUP BY d.year, p.category, c.region, ), QueryBenchmark( 窗口函数, window, SELECT user_id, amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC) as rn FROM orders WHERE created_at 2026-01-01, ), QueryBenchmark( 高基数COUNT DISTINCT, aggregation, SELECT date, countDistinct(user_id) as dau FROM events GROUP BY date ORDER BY date DESC LIMIT 30, ), ] def run_single(self, benchmark: QueryBenchmark, warm_up: int 2, iterations: int 5) - BenchmarkResult: 运行单个基准测试 result BenchmarkResult(benchmarkbenchmark) # Warm up for _ in range(warm_up): try: self._execute_query(benchmark.sql) except Exception: pass # 正式测试 for i in range(iterations): try: start time.perf_counter() self._execute_query(benchmark.sql) elapsed time.perf_counter() - start result.execution_times.append(elapsed) except Exception as e: result.errors.append(fIteration {i}: {e}) return result def _execute_query(self, sql: str): 执行查询子类实现具体引擎的连接和执行 # 这里返回模拟数据实际应连接对应引擎 import random time.sleep(random.uniform(0.01, 0.2)) def run_all(self) - Dict: 运行全部基准测试 results {} for benchmark in self.benchmarks: print(f [{self.engine_name}] 运行: {benchmark.name}...) result self.run_single(benchmark) results[benchmark.name] result status OK if result.success_rate 0.8 else FAIL print(f {status}: avg{result.avg_time*1000:.1f}ms, fp99{result.p99_time*1000:.1f}ms) return results def generate_comparison(self, competitor_results: Dict[str, Dict]) - str: 生成多引擎对比报告 my_results self.run_all() lines [] lines.append( * 80) lines.append(OLAP引擎性能对比报告) lines.append( * 80) # 表头 header f{测试名称:25} {类别:12} header f{self.engine_name:12} for competitor_name in competitor_results: header f{competitor_name:12} header f{最佳:12} lines.append(header) lines.append(- * 80) for benchmark in self.benchmarks: name benchmark.name category benchmark.category row f{name:25} {category:12} times {} my_time my_results[name].avg_time * 1000 if name in my_results else 999 times[self.engine_name] my_time row f{my_time:9.0f}ms best_time my_time best_engine self.engine_name for comp_name, comp_data in competitor_results.items(): comp_time comp_data.get(name, 999) * 1000 if isinstance(comp_data.get(name), (int, float)) else 999 times[comp_name] comp_time row f{comp_time:9.0f}ms if comp_time best_time: best_time comp_time best_engine comp_name row f{best_time:9.0f}ms ({best_engine}) lines.append(row) lines.append(- * 80) # 汇总 lines.append(\n汇总:) for engine_name, engine_times in {self.engine_name: times, **competitor_results}.items(): if isinstance(engine_times, dict): valid_times [t for t in engine_times.values() if isinstance(t, (int, float)) and t 999] else: valid_times [t for t in times.values() if t 999] if valid_times: avg sum(valid_times) / len(valid_times) lines.append(f {engine_name}: 平均 {avg:.0f}ms) return \n.join(lines) if __name__ __main__: # ClickHouse基准 ch_runner OLAPBenchmarkRunner(ClickHouse, {host: localhost, port: 8123}) # 模拟Doris和StarRocks的结果实际应连接真实集群 competitor_results { Doris: { 单表全量聚合: 0.035, 单表时间窗口聚合: 0.042, 两表JOIN聚合: 0.058, 五表星型JOIN: 0.125, 窗口函数: 0.089, 高基数COUNT DISTINCT: 0.067, }, StarRocks: { 单表全量聚合: 0.032, 单表时间窗口聚合: 0.038, 两表JOIN聚合: 0.052, 五表星型JOIN: 0.098, 窗口函数: 0.076, 高基数COUNT DISTINCT: 0.055, } } report ch_runner.generate_comparison(competitor_results) print(report)基准测试工具的设计有一个关键原则测试集必须覆盖不同查询类别。上面的6个测试用例分别覆盖了单表聚合、多表JOIN、窗口函数和高基数COUNT DISTINCT——这些都是真实BI场景中最常见的查询模式。如果只用单表聚合做测试ClickHouse会毫无悬念地胜出但这不能反映真实业务的复杂性。四、三引擎场景适配指南场景ClickHouseDorisStarRocks单表聚合/时序分析最优良良多表JOIN分析一般优最优实时写入查询一般优最优数据更新/删除差良优物化视图自动化手动自动自动运维复杂度中低低社区生态庞大增长快增长快场景适配表之外有几个边界条件需要深入讨论。实时写入与查询的冲突ClickHouse的MergeTree引擎设计为批量写入优化——每次写入会生成一个Data Part后台异步合并。如果写入频率太高如每秒数百次小批量写入会产生大量碎片化的Data Part导致查询性能下降需要扫描更多文件。Doris和StarRocks通过MemTableFlush机制缓解了这个问题支持更高频率的实时写入。在我们的测试中ClickHouse在每秒100次小批量写入时查询延迟增加40%而StarRocks几乎没有影响。数据更新的代价ClickHouse的MergeTree本质上是不可变存储更新数据需要通过ALTER TABLE ... UPDATE异步执行代价是全分区重写。Doris和StarRocks支持主键模型Unique Key Model可以做到行级更新但更新性能仍不如专门的OLTP引擎。如果业务需要频繁更新如订单状态变更建议将OLAP引擎与OLTP引擎分离通过CDC同步变更数据。COUNT DISTINCT的性能差异高基数COUNT DISTINCT是OLAP引擎的经典难题。ClickHouse使用uniqCombined算法基于HyperLogLog精度约98%速度极快。StarRocks和Doris在非精确模式下也使用HLL但在精确COUNT DISTINCT场景下三者都需要全量数据扫描性能差异不大。如果业务允许1-2%的误差建议始终使用非精确模式——性能提升可达10倍以上。冷热数据分层ClickHouse支持TTL表达式自动将冷数据移动到磁盘或删除这是它在这个维度上的优势。Doris和StarRocks也支持冷热分层通过Storage Policy但配置更复杂。在日志分析场景中ClickHouse的TTL自动压缩可以将存储成本降低60-70%。结论OLAP引擎选型建议如果是日志分析、时序数据等单表场景为主ClickHouse是最成熟的选择如果需要多表JOIN的BI分析或实时报表Doris和StarRocks更适合两者中StarRocks在更新性能和生态上略有优势。最重要的建议是用真实业务SQL做POC不要依赖标准Benchmark的数据做决策。从我们的选型实践来看最终选择StarRocks的原因是它在多表JOIN、实时写入和物化视图自动化三个维度上的综合表现最优。但如果业务场景是纯日志分析如ELK替代方案ClickHouse的向量化执行和TTL管理仍然是难以超越的。选型的核心不是找到最强的引擎而是找到与业务查询模式最匹配的引擎。