1. 从“跑分”到“排障”我为什么用TPC-DS折腾ClickHouse和Doris先说结论如果你正在选型OLAP引擎或者已经定了ClickHouse或Doris但被各路“性能对比”文章搞得一头雾水那么自己动手跑一轮TPC-DS可能是最靠谱的验证方式没有之一。我在某跨平台数据系统选型阶段花了三周时间把TPC-DS的99条查询完整压测了ClickHouse和Doris两个引擎期间踩了无数坑也积累了不少一手数据。这篇文章就把整个过程中的思路、步骤、参数调优和排障经验完整记录下来给后续要做同样事情的同学提供一个可以直接“抄作业”的参考。TPC-DS是决策支持类基准测试的事实标准它模拟的是真实的商业分析场景包含星型、雪花型等多维数据模型以及大量复杂的聚合、窗口函数、子查询、CASE WHEN等操作。它不是那种“点一下看谁快”的玩具测试而是能把引擎的优化器、执行器、存储层、内存管理全部压出原形的东西。适合谁来跑三种人一是正在做技术选型的架构师二是负责集群性能调优的DBA或运维三是想深入了解某个引擎底层行为的开发同学。哪怕你是刚入门的数据分析师也能通过这套测试建立起对OLAP引擎性能分化的直观认知。为什么偏偏选ClickHouse和Doris这两个是目前开源OLAP领域关注度极高的代表一个以极致的单表查询性能著称一个以兼容MySQL协议和分布式能力见长。但它们的优化路线差异很大用同一套TPC-DS脚本去跑能非常清晰地展现出各自的取舍和短板。这篇文章不会只给你一堆“谁快谁慢”的结论而是带你完整走一遍从建表、导数据到跑查询、看指标的实操链路并解释每一步背后的原理。2. 测试方案设计先把基准测试的“规矩”定清楚2.1 TPC-DS到底在测什么先简单交代一下TPC-DS的背景方便第一次接触的同学理解。TPC-DS由事务处理性能委员会制定模拟的是零售行业的数据分析场景数据模型包含17张事实表和维度表比如商品、顾客、日期、销售记录、退货记录等。它定义了一套标准的查询模板共99条查询SQL Query这些查询覆盖了报告分析、交互式查询、数据挖掘等不同负载。这套测试最狠的地方在于它用了一个叫“数据倾斜”的机制不是所有数据都均匀分布而是故意制造热点比如某些热门商品、某些活跃用户的记录特别多。这就意味着如果你的引擎在数据分布感知、分区裁剪、聚合下推等环节做不好很容易被“打回原形”。另外TPC-DS的99条查询里有大量嵌套子查询、多级聚合、复杂的CASE WHEN条件这些都是真实数仓报表里常见的写法。很多引擎在简单查询上跑得飞快但一遇到这种“复杂但合理”的SQL优化器就懵了要么生成低效计划要么直接内存溢出。所以TPC-DS的分数本质上衡量的是“处理真实复杂分析查询的能力”而不是单纯看谁能更快地扫描一亿行数据。2.2 选型与版本为什么用这两个发行版我做测试时ClickHouse选择的版本是较新的稳定发行版比如23.x之后Doris选择的是当前主流的2.x版本。这里有个重要提醒OLAP引擎的版本迭代非常快每个月可能都在优化TPC-DS相关的查询所以任何性能对比结果都有“保质期”。建议你在测试时固定好版本号并且在结果中标注清楚避免后续产生误解。ClickHouse我用的是单机多核部署Doris则用了三节点集群。有人可能会说这不公平但在实际生产环境中ClickHouse经常以单机大内存高磁盘吞吐的方式部署而Doris天然是集群架构所以我这么配置是有意模拟各自“最典型的用法”。为了保证对比尽量公平我会在后续分析中区分“单机极限”和“集群扩展”两个维度去看而不是简单地排名。测试数据规模上我选择的是SF100也就是100GB原始数据。这个规模不算大但已经能暴露出不少优化问题。如果你机器配置有限可以先从SF1开始验证流程跑通了再上大数据集。我建议至少要跑到SF100因为小数据量下很多引擎的优化器都“感知不到压力”跑出来的结果没有参考价值。2.3 前置准备机器配置、依赖工具、数据生成测试机的配置如下CPU为32核开启超线程后可识别为64逻辑核内存128GB存储为SSDNVMe操作系统为常见的Linux发行版。这个配置对于SF100来说是够用的如果你只有16核64GB建议把SF降到30否则有些查询可能会因内存不足而失败。数据生成阶段需要用到tpcds-kit这个工具。这是一个官方开源的数据生成器支持生成不同比例的数据集并且能输出ClickHouse和Doris都能识别的标准文本格式。注意tpcds-kit本身生成的SQL模板文件是在某些通用语法基础上的每个引擎的语法细节略有差异所以后面还需要手动适配一部分建表和查询语句。依赖工具还包括一个功能完整的SQL客户端。ClickHouse我直接用原生的clickhouse-clientDoris则用MySQL命令行客户端连接因为Doris兼容MySQL协议另外还需要一个脚本语言环境比如Python或Shell来批量执行查询并采集耗时。3. 建库建表与数据导入最容易翻车的环节3.1 ClickHouse建表用MergeTree还是分布式表这可能是整个测试中态度最需要谨慎的部分。ClickHouse的建表策略直接决定了查询性能你必须针对TPC-DS的查询模式做权衡。我的做法是事实表使用MergeTree引擎并按日期字段进行分区维度表使用MergeTree但通常不分区分得太细避免小文件过多。核心的注意点在排序键ORDER BY上。ClickHouse的MergeTree是“按排序键有序存储”的这意味着排序列能实现高效的列裁剪和索引过滤。TPC-DS的查询大部分会带时间条件比如“某个日期区间的销售情况”所以事实表排序键的第一位必须放日期的衍生字段比如d_date_sk对应的日期能把扫描量缩小几个数量级。还有一个选择是做分布式表还是本地表。如果ClickHouse是单机部署直接用本地表就行如果是集群则需要创建Distributed表作为查询入口。我测试时虽然只有一台机器但为了模拟生产环境还是建了分布式表只有一个分片这样后续上集群时不需要改SQL。建表语句的关键部分大致类似这样CREATE TABLE clickhouse_db.store_sales ( ss_sold_date_sk Int32, ss_item_sk Int32, ss_customer_sk Int32, ss_quantity Int32, ss_list_price Decimal(7,2), ss_sales_price Decimal(7,2), ss_net_paid Decimal(7,2), ... ) ENGINE MergeTree PARTITION BY (toYYYYMM(toDate(ss_sold_date_sk))) ORDER BY (ss_sold_date_sk, ss_item_sk, ss_customer_sk) SETTINGS index_granularity 8192;这里我特意把ss_sold_date_sk放在ORDER BY第一位并且PARTITION BY使用了月份实测下来大部分带日期过滤的查询都能精确跳过无关分区效果极其明显。3.2 Doris建表数据模型选择与分桶策略Doris的建表思路完全是另一套体系。Doris支持三种数据模型Duplicate、Aggregate、Unique。对于TPC-DS这种以事实表和维度表为主的场景我建议事实表用Duplicate模型保留所有原始记录不聚合方便各类Metrics计算维度表也用Duplicate即可。如果用了Aggregate模型反而会限制查询灵活性因为聚合模型要求所有非Key列设定聚合函数很多SQL语句里的复杂表达式会变得非常别扭。分桶是Doris性能的重中之重。Doris默认使用Random分桶但随机分桶对范围过滤并不友好。我的建议是选择某个高基数列作为分桶列比如订单编号或者商品编号分桶数建议设置为集群BE节点数量的倍数并且确保数据分布均匀。我们当时用了20个分桶桶数3节点集群配合副本数为1测试场景不需要高可用。Doris建表语句示例CREATE TABLE doris_db.store_sales ( ss_sold_date_sk INT, ss_item_sk INT, ss_customer_sk INT, ss_quantity INT, ss_list_price DECIMAL(7,2), ss_sales_price DECIMAL(7,2), ss_net_paid DECIMAL(7,2), ... ) DUPLICATE KEY(ss_sold_date_sk, ss_item_sk, ss_customer_sk) PARTITION BY RANGE(ss_sold_date_sk) DISTRIBUTED BY HASH(ss_item_sk) BUCKETS 20 PROPERTIES (replication_num 1);Doris建表时还要注意max_buckets_per_partition等限制以及分桶列选择不当导致的数据倾斜。我们当时发现如果分桶列选ss_customer_sk有些桶的数据量能差出5倍后续查询就会明显受到长尾效应拖累。3.3 数据导入从生成到入库的完整流水线数据生成使用tpcds-kit下的dsdgen工具指定目录和并行度即可。并行度我通常设为CPU核数的一半比如32核机器设16个并行这样生成速度很快还不会把磁盘IO占满。dsdgen -scale 100 -dir ./data -parallel 16 -child 16生成出来的是一堆.dat文件每个表一个字段之间用分隔符分开。ClickHouse和Doris导入方式不同ClickHouse支持直接INSERT FROM INFILE也支持通过clickhouse-client的--input-format参数但最快的方式是用clickhouse-local做ETL后再导入。我实际用的是最简单的方案将.dat文件先清理一下特殊字符然后用INSERT INTO ... FORMAT TSV批量导入。不过为了加速建议用clickhouse的INSERT语句配合多行批量或者直接使用clickhouse-client -q INSERT INTO ... FORMAT TSV file.dat。Doris这边由于它兼容MySQL协议最常用的是通过Stream Load或者Broker Load导入。我推荐Stream Load因为不需要额外部署Broker一条curl命令即可curl --location-trusted -u user:password -H label:load_store_sales_001 \ -H column_separator:| \ -T store_sales.dat \ http://fe_host:8030/api/doris_db/store_sales/_stream_load注意tpcds-kit生成的.dat文件默认分隔符是|而Doris Stream Load默认也是|所以不需要转换但字段的结束符可能带有换行符整体导入没有问题。导入过程中我踩了一个大坑ClickHouse默认的日期格式是YYYY-MM-DD而TPC-DS生成的是整数SK如2450815。在导入时如果直接把整数塞给Date类型会报错。所以我建表时把日期相关字段都定义为Int32方便导入查询时再用函数转换。这也提醒各位TPC-DS的日期字段并不是SQL标准DATE类型而是代理键你需要根据引擎语法做适配。导入完成后务必执行ANALYZE或统计信息更新操作尤其是Doris它依赖统计信息生成执行计划ClickHouse虽然统计信息要求不严格但建议还是要做一下完好的片段合并OPTIMIZE避免太多小文件影响扫描性能。4. 查询适配与执行策略99条SQL逐条过4.1 为什么不能直接照搬TPC-DS的SQLTPC-DS官方提供的99条查询模板带有大量模板变量和标准SQL语法直接拿到ClickHouse和Doris上跑大概率会报错。原因主要有三点第一函数名和语法差异。比如日期加减标准写法是date_add(day, 30, d_date)ClickHouse写dateAdd(d_date, INTERVAL 30 DAY)Doris写date_add(d_date, INTERVAL 30 DAY)虽然都支持但模板里用的占位符需要替换成实际列名。第二子查询与CTE的处理方式不同。TPC-DS模板里大量使用WITH子句ClickHouse虽然支持CTE但对于某些嵌套的派生表优化器可能会直接展开并在内存中物化导致性能问题。Doris的优化器对CTE的处理比较友好但某些子查询关联条件也得手工调整HINT。第三类型推断差异。比如模板里ss_net_paid * 1.00与整数列相乘在Doris里会生成DECIMAL类型而在ClickHouse中可能是浮点类型最终结果精度有差异。为了保持测试一致性我统一把涉及金额的字段全部定义为DECIMAL(7,2)并在查询中显式转换避免因类型推断不同导致结果集不一致。4.2 批量执行与结果采集的工具脚本我不建议一条一条手工跑。写一个简单的Python脚本读取SQL文件列表依次对两个引擎执行记录每次执行时间和返回行数。需要注意超时设置有些查询在某个引擎上可能极慢甚至几分钟出不来所以要设计时间阈值比如超过600秒就杀掉并标记为超时。ClickHouse执行使用clickhouse_driver库Doris使用pymysql。具体脚本框架如下import time from clickhouse_driver import Client as CHClient import pymysql ch_client CHClient(hostlocalhost, userdefault, password, databaseclickhouse_db) doris_conn pymysql.connect(hostfe_host, port9030, useruser, passwordpass, databasedoris_db) def run_ch(sql): start time.time() result ch_client.execute(sql, settings{max_execution_time: 600}) return time.time() - start, len(result) def run_doris(sql): cursor doris_conn.cursor() start time.time() cursor.execute(sql) rows cursor.fetchall() return time.time() - start, len(rows)然后遍历SQL文件把每个查询的耗时记录到CSV里。为了排除偶发因素每条查询我建议至少跑三遍取最小值——因为OLAP引擎第一次执行可能涉及数据加载和缓存建立之后会更快这种预热后的数据更贴近真实业务中稳定状态的表现。4.3 针对ClickHouse的语法调整实例分享一条典型查询的适配过程。比如TPC-DS的query64逻辑是计算各品类的销售排名变化涉及到多个子查询和窗口函数。原始模板里有一段类似WITH ss AS ( SELECT s_store_sk, SUM(ss_net_paid) AS revenue FROM store_sales, date_dim, store WHERE ss_sold_date_sk d_date_sk AND d_month_seq BETWEEN 1200 AND 1211 AND ss_store_sk s_store_sk GROUP BY s_store_sk ) ...在ClickHouse中CTE没问题但GROUP BY后跟ORDER BY配合LIMIT时某些引擎会出现“内存排序过大”的问题。我给ClickHouse的优化方式是把中间结果物化成临时表CREATE TEMPORARY TABLE AS SELECT避免优化器反复内联CTE。实际测试中有些查询用物化临时表比直接写子查询快出3倍以上这个经验可以推广到其他引擎。4.4 针对Doris的语法调整实例Doris本身对标准SQL的支持度较高但TPC-DS有些查询用了rollup或cube这类操作Doris未必支持。repalce成等价的GROUP BY UNION ALL是个笨办法但对于测试来说我们要保证查询语义一致所以不管引擎支不支持统一改写成标准GROUP BY集合。还有一个很棘手的问题是Doris的TIME或DATETIME函数与日期SK的转换。好在Doris支持直接CAST(2450815 AS DATE)这会转换成从1970-01-01算起的第2450815天我们可以利用这一点与date_dim表的d_date列关联。我这里再强调一个容易踩的坑Doris的默认执行调度可能是单并发尤其是客户端连接没有开启并行执行时。如果你用MySQL命令行执行长查询默认它会等执行完成才返回这没什么问题但要注意Doris关闭结果集缓冲的方式否则边执行边返回会影响耗时统计。建议客户端加上fetchsize-1之类的参数具体取决于驱动让服务端全量计算完再返回。5. 性能对比结果解读快出来的时间都花在了哪里5.1 整体耗时分布与排名我们跑完99条查询后统计了各查询的最短执行时间。按照TPC-DS的惯例取Q01到Q99所有查询的总耗时也可以按几何平均来计算。这里我不贴出全量数据但可以描述两个大趋势首先ClickHouse在大部分“单表扫描聚合”型查询上表现极其出色特别是涉及分区裁剪明显的查询耗时能达到Doris的三分之一甚至更低。这与ClickHouse的列式存储、稀疏索引、向量化执行是直接相关的。其次Doris在多表关联特别是大事实表与多个维度表JOIN的场景下表现稳定尤其当数据分布均匀时它的MPP架构能充分利用多节点并行。而ClickHouse在复杂多表JOIN上容易退化为“右表加载内存左表流式扫描”的模式如果右表过大性能就会明显恶化甚至OOM。不同的查询有明显的“胜负分水岭”。为了直观展示我做了个简单的分类查询模式典型查询编号ClickHouse表现Doris表现大表聚合日期过滤Q1-Q10极快中等多表JOIN2-3张Q20-Q30一般快多个复杂窗口函数Q40-Q50较慢中等大量CASE WHENQ60-Q70快快多层嵌套子查询Q80-Q90慢中等这说明什么如果一个业务以单表日志分析为主ClickHouse效率极高如果业务是传统数仓的星型模型多表分析Doris会更顺手。两者没有绝对的优劣只有场景匹配。5.2 深入分析几次“翻车”的查询挑两个典型查询细说。第一个是Q18它在TPC-DS里涉及大范围的日期JOIN和两个大表的关联同时还有CASE WHEN对销售额按区间分档。在ClickHouse上如果执行计划选择把date_dim作为一个广播表加载到内存一旦date_dim的过滤条件太弱比如只选了某个年份整个维度表进入内存循环查找就会吃掉大量CPU缓存。实测里这条在ClickHouse上比Doris慢了一个数量级。后来我尝试给ClickHouse增加join_algorithm partial_merge才勉强拉回一些性能。第二个是Q72它用了多个WITH子查询和UNION ALLDoris的优化器能较好地处理这种多路UNION的分阶段执行而ClickHouse的UNION ALL分支是并行执行的但在合并过程中会因为数据分布不均出现某个处理节点跑满、其他节点空闲的情况。这条我们在ClickHouse中通过手动加max_threads参数调优后仍有约2倍差距。这些细节表明简单的“总耗时排名”并不能体现真实使用中需要付出的调优工作量。我给所有要做测试的同行提个醒千万不要只跑一遍默认配置就下结论一定要针对每条查询做合理的参数调整因为生产环境的任务调度和并发控制也是可以调整的。5.3 资源消耗与可扩展性观察除了耗时我还观察了CPU、内存和磁盘IO。ClickHouse在查询时CPU利用率通常能达到80%以上说明向量化执行发挥了作用但内存占用在一定复杂查询下容易飙升特别是带有大的ORDER BY或者JOIN时。而Doris因为有BE节点分摊单节点内存相对平稳但集群网络会成为瓶颈尤其是Shuffle量大的查询。在扩展性方面我用相同的TPC-DS数据集分别在ClickHouse单机和Doris三节点上验证发现Doris通过增加BE节点大多查询耗时能线性下降而ClickHouse的单机性能上限很高但分布式部署的复杂度比Doris高不少需要手动管理数据重分布、Replicated表副本等操作。如果预期集群规模会持续扩张Doris的运维成本会更低如果大部分压测查询都在单机可控范围内ClickHouse的性价比非常突出。6. 常见问题与排查技巧实录6.1 数据导入阶段字符编码与时区污染TPC-DS生成的.dat文件是纯文本没有特殊编码问题但如果从Windows环境拷贝到Linux可能带上\r\r导致解析错误。解决方式很简单用sed去掉多余的回车符号或者导入前统一转换。另外日期字段在Doris里如果定义为DATE类型而文件里是整数导入会直接报错所以要么在ETL阶段将整数转成日期字符串要么建表时就用BIGINT存储查询时再转换。我建议所有参与测试的字段能保持原始整数类型就保持不要为了“好看”强行转成标准类型这样能减少一半的导入报错。6.2 查询执行超时如何判断是慢还是卡死有些查询在高并发下迟迟不返回可能是因为资源等待。ClickHouse可以用system.processes表查看当前活动查询杀掉长期运行的进程Doris可以用SHOW PROC /current_queries或者查看FE审计日志。如果一条查询执行超过预期时间的3倍大概率是执行计划有问题而不是数据量大这时先杀任务再分析计划。我用过最有效的定位工具是查看执行计划EXPLAIN。ClickHouse的EXPLAIN能输出完整的执行树Doris的EXPLAIN能看到每个扫描算子的行数估算。通过对比估算行数和实际行数很快能发现Join顺序错乱、过滤条件下推失败的问题。6.3 内存溢出与并发参数调优ClickHouse默认内存限制比较宽复杂JOIN很容易把内存打爆。我建议提前在users.xml里配置max_memory_usage为物理内存的70%左右并给每条查询会话设置max_memory_usage_for_user。Doris则需要注意BE的buffer pool大小一旦查询并发高容易触发memory limit exceeded这通常不是Bug而是说明你的分桶太多或执行并发太高。调低parallel_fragment_exec_instance_num趁早。6.4 结果正确性比对防止“跑得快但跑得错”性能测试的前提是没有优化错结果。我写了一个校验脚本对同一SQL在两个引擎上的返回结果进行数值对比。TPC-DS官方允许结果误差范围在某些聚合查询上是1%以内的浮点偏差但大部分整数类聚合必须完全一致。如果我们发现结果不一致会先检查精度问题再检查是否因为隐式类型转换导致。这个问题在金额字段是DECIMAL的时候尤其重要。经过排查有两类差异经常出现一是ClickHouse的count(distinct)在极高基数时是基于近似算法如果没显式设置count_distinct实现二是Doris在某些聚合上的舍入方式与其他数据库不同。所以查询里我统一使用uniqExact替代ClickHouse默认的uniqDoris则保持count(distinct)并确保用DECIMAL做金额运算最终才能实现全对。6.5 测试过程中的时间成本控制跑99条查询每条三遍再加上各种参数调整听起来很耗时。我的经验是先并行跑一遍全量找出尤其慢的10条和时报错的查询集中分析执行计划调整后只重跑这10条其他查询保留首轮结果。这样能节省一半以上时间又不影响结论判断。7. 测试结论之外的思考7.1 选型建议要落到业务模型上经过这轮TPC-DS实测我最大感受是性能跑分的意义不在于“分出高低”而在于“看出特性”。就这个版本的测试来看ClickHouse在宽表平铺、日志分析、监控指标聚合这类场景中表现亮眼Doris在星型模型、复杂JOIN、实时预聚合以及需要兼容MySQL生态的团队中更有优势。如果你的业务大量使用标准JDBC/ODBC连接、现有工具链都以MySQL为主Doris的平滑迁移价值比单纯跑分上的差距重要得多。7.2 测试本身的局限与后续扩展TPC-DS只覆盖了离线批量分析场景没有包含点查询、高并发小查询、数据实时写入和更新场景。如果选型还涉及实时数仓或交互式报表建议补测TPC-H简单模型以及互联网企业常使用的自建查询集比如高QPS的明细点查、指定主键更新等。我自己在选型结束后又额外跑了两轮针对业务自定义的压测才完全确定最终的引擎。这样组合验证下来才更贴合生产环境。7.3 一个值得尝试的方向混合部署测试过程中我还发现ClickHouse与Doris并不必然是“二选一”的关系。有些团队把实时明细数据放在ClickHouse把维度建模和SQL复杂分析放在Doris中间通过统一的同步链路打通。这种混合架构在真实场景中并不少见如果你有资源和精力可以基于TPC-DS的数据分别做一次模拟验证跨引擎协同的可行性。8. 最后分享两个小技巧一个是建表时不要偷懒把每个字段的注释写清楚这会让你后续排查数据差异时省下大量时间。另一个是在采集执行时间时记录一下查询结果行数有时候两个引擎执行都很慢但一个返回100行一个返回10万行真实效率不一定像时长差距那么大。用行数归一化后再分析会得到更客观的结论。跑完这轮TPC-DS我不敢说彻底摸透两个引擎但至少对它们的脾性有了相对清晰的认知。后续如果再遇到选型问题我还会先跑一次基准测试用数据说话而不是被网上各种争执带着走。希望这篇文章也能给你的技术决策带来一点实实在在的帮助。