Doris与StarRocks选型对比:返利BI系统从MySQL迁移OLAP的实践与踩坑

发布时间:2026/10/1 15:10:45
Doris与StarRocks选型对比:返利BI系统从MySQL迁移OLAP的实践与踩坑
去年年底我们返利BI系统做了一次底层引擎的替换把原来撑了快两年的MySQL汇总表方案换成了OLAP引擎。当时摆在面前的三条路是Doris、StarRocks和ClickHouse经过两周压测和一个月并行试运行最终圈定了Doris与StarRocks继续做深度选型评估。这篇文章就把我们当时做选型决策的思路、同一套返利数据集上的性能对比结果以及迁移过程中几个典型的踩坑记录完整拆开来讲希望能给正在做OLAP引擎选型或者准备把BI底层迁移到列式存储的同学一些参考。需要说明的是Doris和StarRocks本是同源产品技术脉络纠缠得很深网上很多文章喜欢分个高下但我们实践下来的感受是两者的差异更多体现在业务场景的匹配度上而不是绝对性能的优劣。下面我会从业务痛点、架构差异、实测数据、建模细节和迁移踩坑五个方面展开尽量把“为什么这样选”“实际用起来有什么差别”讲清楚。1. 从业务痛点倒推技术选型为什么返利BI需要OLAP引擎1.1 返利系统最典型的分析查询长什么样先交代一下我们系统的业务背景。电商返利本质上是一种按效果付费的联盟营销用户通过推广链接下单订单完成后平台给推广渠道计算并发放佣金。返利BI系统的核心就是围绕“订单—用户—商品—渠道—结算单”这几张表做多维分析。表结构大概长这样CREATE TABLE refund_order_fact ( order_id BIGINT, user_id BIGINT, goods_id BIGINT, channel_id BIGINT, order_time DATETIME, settle_date DATE, order_amount DECIMAL(18,2), settle_amount DECIMAL(18,2), commission_rate DECIMAL(5,4), status TINYINT -- 1待结算 2已结算 3已失效 ) ENGINE OLAP DUPLICATE KEY(order_id) DISTRIBUTED BY HASH(order_id) BUCKETS 24;BI看板里最常见的查询是这几类按天、按渠道汇总佣金金额和订单量指定时间窗口内的Top N商品排行按用户首单率、复购率做漏斗分析某个渠道的月度佣金环比、同比财务结算前对账单明细做对账核对这类查询有一个共性特征单表扫描量大、聚合维度多、过滤条件随机组合。比如“统计最近30天每个渠道、每个一级类目下的有效佣金总额并按照渠道分组对比上个月同期”这条SQL在MySQL里要扫几千万行订单明细再走临时表和文件排序跑一次要几十秒。如果看板上有十几个图表同时刷新数据库直接卡死。1.2 MySQL加汇总表的极限在哪我们早期的方案是“MySQL主库 定时任务 多张汇总表”。运营看板需要的每个指标组合都提前用定时任务算好写进一张带复合索引的汇总表前端查询时只做简单group by不碰明细。这套方案在业务量小的时候很稳但当维度组合多起来之后问题就暴露了第一汇总表膨胀得非常快。渠道、商品类目、用户等级、时间周期每增加一个分析维度就要新建一组汇总表来预聚合否则查询就得临时扫明细性能立刻掉下来。后期我们光汇总表就有四十多张ETL任务之间的依赖关系复杂到换个字段都要小心翼翼。第二口径经常对不上。定时任务先跑渠道维度、再跑商品维度中间如果有一批返利记录发生状态变更比如团长佣金比例调整导致结算单重算不同汇总表之间的数据就会产生不一致。运营和财务各看一套数字反复找人排查非常内耗。第三实时性不满足。所有报表都是T1每天凌晨跑批。返利业务很依赖“今日实时佣金”这种指标团长要看实时推广效果运营要根据实时数据调整活动策略。T1根本没法支撑。所以我们决定引入OLAP引擎核心诉求是明细数据直接入库不再依赖多套预聚合汇总表支持高并发多维即席查询单条复杂聚集控制在秒级返回能够处理返利记录的状态异步更新而不是全量重刷导入链路要简单最好能兼容已有的数据管道在这个前提下Hive和Spark离线数仓虽然很成熟但查询延迟和数据更新链路都太重不适合放在BI看板后面直接面向运营。ClickHouse的单表扫描性能确实很强但多表join优化、点更新和数据一致性方面有明显短板。最终进入决赛圈的就是Doris和StarRocks。这里多说一句选型的思考方式。不要一上来就比性能跑分性能只是入场券。真正重要的是先把自己的查询特征、数据更新模式、运维能力边界列清楚再拿产品和自己的场景匹配。性能对比是最后一步的验证而不是第一步的筛选条件。2. Doris与StarRocks同源不同路架构差异如何影响查询表现2.1 同源分支的时间线Doris和StarRocks的关系很多刚接触的同学容易搞混。Doris的前身是百度开源的Palo后来捐献给Apache基金会成为Apache Doris。StarRocks则是2018年前后从Doris早期版本分叉出去、独立发展起来的另一个分支所以两者在基础理念上有大量相似之处都是MPP架构、列式存储、向量化执行、支持MySQL协议、使用FE和BE的两层无共享架构。但在分叉之后两个项目走了不一样的技术路线。StarRocks更激进从1.x版本就开始强调全面向量化执行和自研的CBO优化器2.x之后又重点做主键表和物化视图Doris则更偏向稳扎稳打2.0版本才完成查询执行器的全面向量化但社区生态更庞大周边工具和大厂案例更多。所以选型时很多人会问“Doris和StarRocks到底谁强”这个提问本身没法回答。更准确的问法是以我们当前的业务场景哪个产品的架构取舍更接近我们的需求。2.2 查询引擎与优化器的关键差异先说执行引擎。StarRocks从1.19开始就把向量化执行作为默认执行方式Doris在2.0之后也全面向量化了两者在单条大SQL的扫描和聚合能力上差距已经不大。但在复杂查询、多表join的执行计划选择上StarRocks的自研CBO在统计信息收集和代价估算上做得更细尤其在多表关联、子查询解构这类场景中误选执行计划的概率明显更小。我们的实测程序员也能直观感受到这一点。同样的“渠道、商品类目、用户等级三维关联聚合”查询Doris偶尔会把join顺序排错导致中间结果集膨胀内存和耗时飙升StarRocks整体上更稳定一些大多数情况下执行计划都比较合理。另外StarRocks有一个很贴近BI场景的特性——查询并发度管理。Doris默认的查询并发能力并不差但在高并发小查询场景下StarRocks的Pipeline执行引擎配合资源组限制能把不同部门、不同优先级的查询做资源隔离避免运营那边一个跑大聚合的看板查询把财务对账用的核心查询拖垮。这一点我们在两个引擎的对比测试里体会非常明显。2.3 数据模型与更新能力返利场景的关键差异返利BI有个很特殊的数据特征订单明细不是生成后就不变的。用户在确认收货前订单可能取消退款之后返利要回收结算周期到了佣金状态要从“待结算”变成“已结算”甚至运营调整某个渠道的佣金比例后历史佣金要重算回滚。这就要求OLAP引擎具备比较高效的更新能力。Doris提供的模型有三种Duplicate、Aggregate、Unique。Unique模型通过Merge-On-Read实现更新但在高并发小批量更新场景下性能表现比较一般。StarRocks除了这三种模型之外还做了一张主键表Primary Key表通过删除标记加写入新版本的方式更新底层实际上把Merge操作尽可能推迟到读取阶段并且支持了实时更新场景的写入优化。在我们的返利佣金重算场景里每天大概有几十万到几百万条结算单状态更新。如果使用Doris的Unique模型重算时段的查询经常出现读放大明细查询变慢换到StarRocks主键表之后同样的更新负载下查询稳定性明显改善。如果你的业务里像“订单状态频繁流转”这种实时更新需求很强这一点值得重点测试甚至应该提前设计好压测场景。2.4 物化视图与外表查询的取舍原本我们是打算用物化视图解决一部分高并发报表问题的所以在对比时对两边的物化视图也做了关注。Doris在2.1版本之前提供同步物化视图使用起来相对简单创建后写入时自动维护但限制也比较多比如不能有join、聚合函数支持有限。StarRocks的物化视图属于异步刷新模式支持多表join、支持分区刷新同时还能在查询时自动改写匹配功能上更像传统数仓的物化视图灵活度高不少。实际使用下来我们最终没有大规模依赖物化视图主要原因是返利明细的更新方式太灵活维护异步物化视图的刷新逻辑需要额外的成本。最终我们选择“明细表 定时DWS层轻汇总 查询侧改写”的组合。但如果你团队对物化视图模式驾轻就熟且业务更新模式相对固定StarRocks在这方面会有更好的可玩性。再提一点外表查询。两个产品都支持通过Catalog方式访问外部数据源可以跨源查询MySQL、Hive、Iceberg等。对我们来说BI底表进OLAP后仍需要跟MySQL里的维表做关联Doris和StarRocks都支持外表关联实际体验差别不大。这个能力能减轻迁移初期的压力不用一口气把所有表都搬进来。3. 实测对比同一套返利数据集上的查询性能与稳定性3.1 测试环境与数据集构造选型阶段我们在同一个测试集群上部署了两套引擎测试配置保持一致组件配置FE节点3台16C64G万兆网卡BE节点3台16C64GNVMe SSD 2TB操作系统CentOS 7.9内核5.4部署模式3副本生产级部署测试数据取了我们线上近18个月的返利订单明细共5.2亿行压缩后约80GB导入完成后包含订单维度、用户维度、商品维度、渠道维度、结算单维度共5张表其中最大的一张返利明细表对应了典型的星型模型。导入方式统一使用Stream Load每个文件约512MB并发导入。测试口径上我们做了三组冷查询先执行一次预热查询绕开文件页缓存、热查询连续执行取后10次的平均值、以及并发混合场景模拟BI看板上8种查询同时刷新用户并发数分别为10、30、50。3.2 关键SQL与耗时对比下面列出测试结果最典型的四条SQL都是返利BI看板上真实在用的查询A按天统计有效佣金总额和订单量SELECT settle_date, SUM(settle_amount) AS valid_commission, COUNT(DISTINCT order_id) AS order_cnt FROM refund_order_fact WHERE status 2 GROUP BY settle_date ORDER BY settle_date;查询B某时间段内各渠道类目的佣金排行SELECT channel_id, goods_category, SUM(settle_amount) AS commission FROM refund_order_fact WHERE settle_date 2025-01-01 AND settle_date 2025-04-01 AND status IN (1, 2) GROUP BY channel_id, goods_category ORDER BY commission DESC LIMIT 100;查询C跨表join统计每个渠道近半年的首单用户数和二次复购率SELECT f.channel_id, COUNT(DISTINCT f.user_id) AS first_order_users, SUM(CASE WHEN r.rebuy_flag 1 THEN 1 ELSE 0 END) AS rebuy_users FROM refund_order_fact f LEFT JOIN dim_channel c ON f.channel_id c.channel_id LEFT JOIN rebuy_summary r ON f.user_id r.user_id WHERE f.settle_date 2024-10-01 GROUP BY f.channel_id;查询D高并发下重复执行查询B模拟同一时间多个运营人员并发查看。热查询的平均耗时数据如下查询Doris 平均耗时StarRocks 平均耗时备注查询A0.82s0.61s单表全扫列存压缩优势明显查询B2.45s1.98s两维聚合StarRocks略优查询C8.12s5.43s多表join差距最大的一条查询D50并发下P95达41s50并发下P95达19s并发场景差距非常明显从数据上看单表扫描场景Doris和StarRocks差距并不大简单聚合时差距在20%到30%之间但一旦涉及多表join或者并发压力上来StarRocks的领先幅度就拉大了。原因还是前面说的CBO和Pipeline执行引擎的差异。值得注意的是这些数据和测试集群配置、数据分布强相关不代表所有场景下的结论。如果你的业务大量集中在单表大查询、不怎么做多表join那这两者之间可能不会拉开这么明显的差距。3.3 并发、稳定性与资源占用除了耗时我们还重点看了并发和稳定性。50并发混合场景下Doris出现了一次因内存不足触发的查询失败BE节点内存使用率飙到92%部分查询直接报错StarRocks在同样的并发下内存使用率稳定在75%左右虽然没有报错但P95延迟也明显上涨。这个结果给我们一个重要提示如果BI系统会面向比较多运营人员同时使用最好一开始就按并发需求做资源规划不要只看单条SQL耗时。平台顺手测了导入和压缩率。同样5.2亿行数据Doris压缩后约82GBStarRocks约76GB。导入性能方面Stream Load并发12个文件两者吞吐都在120MB/s以上日常使用感知不到明显差异。跑完这些测试之后我们的判断是Doris完全能满足业务的大部分场景性能也够用StarRocks在复杂查询和并发场景表现更好而这两点恰恰是电商返利BI在运营看板高负载时最容易被投诉的点。所以最终选定StarRocks作为生产引擎同时保留了Doris的对比环境继续跟踪版本进展。4. 数据导入、分桶策略与字段演进决定长期体验的实操细节4.1 Stream Load的两种方式与Java调用细节决定用StarRocks之后第一批要解决的就是数据导入。我们从MySQL binlog解析出数据变更经过Kafka、Flink做实时清洗再通过Stream Load写入StarRocks。相比Broker LoadStream Load更适合实时、大批次高频导入不需要Hadoop集群依赖直接通过HTTP接口发送数据。Java调用Stream Load的标准写法大概是这样的String url http://fe_host:8030/api/db_name/refund_order_fact/_stream_load; HttpPut put new HttpPut(url); put.setHeader(Authorization, Basic base64Encode(user:password)); put.setHeader(Expect, 100-continue); put.setHeader(Content-Type, application/json); put.setHeader(format, json); put.setHeader(strip_outer_array, true); put.setHeader(label, load_label_ System.currentTimeMillis()); JSONArray dataArray new JSONArray(); // 组装一批数据比如每批10000条 for (RefundOrder order : batchList) { dataArray.put(order.toJsonObject()); } StringEntity entity new StringEntity(dataArray.toString(), UTF-8); put.setEntity(entity); try (CloseableHttpResponse response httpClient.execute(put)) { String respBody EntityUtils.toString(response.getEntity(), UTF-8); JSONObject result JSON.parseObject(respBody); if (result.getIntValue(Status) ! 200) { // 处理失败读取错误URL日志排查 } }这里有几个容易踩的细节第一label一定要设置而且要保证集群内唯一。Stream Load的label是幂等标识如果导入过程中网络超时导致客户端没收到响应你可以拿着同一个label重试导入引擎会自动去重。如果省略label重试时就会产生重复数据对返利金额这种敏感指标是绝对不能接受的。第二每批数据量要控制好。我们测试下来的经验是单批JSON大小在10MB到50MB之间比较稳超过100MB后导入失败重试的成本会明显上升。数据量太小时导入吞吐上不去造成大量小事务合并压力。这个值也和集群磁盘IO能力有关需要实测调整。第三从Flink写入时不要每个checkpoint都触发一次Stream Load根据业务容忍的延迟合理设置flush间隔。我们线上设置的是每5秒或每10万条flush一次兼顾了实时性和导入性能。补充一点如果是启动阶段要做历史数据回填建议直接用Broker Load把HDFS上的历史文件批量导入比Stream Load一条条推更高效。我们回填5.2亿行历史数据用Broker Load并发拉起12个任务大概一个半小时导完。4.2 分桶数怎么定几MB数据要不要分桶分桶是网上问得很频繁的一个问题尤其是“我数据只有几MB是不是不需要分桶”。很多新手在这个环节会走进两个极端要么每个表都照搬默认分桶数要么觉得数据小就不设分桶。先说结论分桶不是为了“把数据拆开好看”而是为了三个目的——控制单个Tablet的体积、提供并行扫描的粒度、保证数据在BE节点间的均衡分布。所以“数据小就不分桶”的想法不完全对但也确实不需要为了分桶而分桶。分桶数的经验估算公式社区里比较一致的做法是单Tablet数据量控制在300MB到1GB之间比较合适分桶数建议取BE节点数的整数倍这样能让数据均匀分布到所有BE假设你机器上数据总量大约80GB副本3份那实际上底层存储约240GB除以500MB的Tablet目标大小大约480个Tablet。如果集群有6个BE节点分桶数取480或512都可以既能保证每个Tablet不过大又能让并行度足够。但如果你整个表只有几MB强制分成几十个桶反而有害。每个Tablet都有独立的元数据Tablet多了元数据管理成本上升导入时会产生大量小文件查询时并行扫描的优势又发挥不出来。对于这种小数据量建议直接建表时分1到3个桶甚至不设置分桶让引擎按默认策略处理。我们的一个实际经验是返利明细这种增长很快的大表要提前按未来一年的数据量估算分桶数而维度表这种基本不增长的表保持小分桶数即可。如果后续发现某些Tablet过大或查询热点集中在个别桶上还可以通过动态分桶或重建表的方案调整。另外要注意分桶列的选取。我们返利明细表选的是order_id因为查询几乎都会带订单维度过滤而且order_id的分布足够离散。如果你按user_id分桶某个大渠道的用户容易被哈希到同一个桶造成数据倾斜影响并行扫描效果。4.3 字段变更、资源分配与日常管理迁移过程中还处理了不少建表之外的管理工作。字段改名是经常遇到的需求。StarRocks里可以用一条命令完成ALTER TABLE db_name.refund_order_fact RENAME COLUMN goods_category TO category_name;要注意的是改名操作会有短暂的元数据变更锁如果表正在被高频查询最好安排在低峰期执行。Doris也有类似语法但版本之间有差异上线前先在小表上验证一次比较稳妥。“用户资源分配”也是我们重点配置的功能。StarRocks的资源组WorkGroup可以把不同查询映射到不同的CPU和内存配额CREATE WORKGROUP wg_bi_big_query WITH ( cpu_core_limit 8, mem_limit 30% );我们给运营看板的大聚合查询设置了独立的资源组并把内存限制压在30%以内这样即使有人写了个慢查询也不会把整个集群拖死。日常运维上Doris和StarRocks类似FE负责元数据和查询规划BE负责数据存储和查询执行。部署时建议FE节点至少3台做高可用BE节点单独部署不要和FE混部避免资源竞争。磁盘方面BE节点的数据目录最好规划独立分区日志目录和数据目录分开避免日志涨满导致BE异常退出。如果是从Doris迁移到StarRocks特别是用了他家生态工具的同学要留意版本兼容问题。SelectDB、DataX等周边工具的版本要跟StarRocks版本匹配好否则可能出现连接异常或协议不兼容这个我们后面专门排查过。5. 从踩坑到收益迁移过程中的典型问题与优化建议5.1 跨引擎查询的missing相关错误排查迁移期间最让人头疼的一类报错是跨引擎查询时出现类似“missing xxx”的错误提示。不是Doris本身报错而是我们通过Presto连接Doris的Catalog做联邦查询时偶尔会抛出找不到列、找不到函数的异常。这类问题的排查链路值得记录一下。现象Presto查询Doris外表时偶尔报“Column goods_category cannot be resolved”或“Function date_format is missing”。初步定位先确认Doris源表里的字段和Presto提交的SQL字段是否完全一致。我们遇到的一个真实问题就是源表里字段叫goods_category但Presto侧缓存了旧的元数据认为是goods_cat两者对不上。进一步排查Presto和Doris之间走的是Catalog方式映射涉及元数据同步。如果源表做过字段变更而Presto的元数据缓存没有刷新就容易出现missing类错误。解决方法是刷新Presto侧的Catalog缓存或者在Doris侧通过INVALIDATE METADATA强制刷新。还有一个很容易被忽略的原因是大小写敏感。Doris默认把表名字段名存储为小写Presto的SQL如果写了大写字段名在某些连接器配置下会直接无法解析。我们后来统一约定跨引擎查询的SQL字段全部小写并且写完SQL先在Doris客户端原样执行一遍确认没问题再到Presto里跑。遇到这类报错时建议按这个顺序排查确认字段名、表名是否完全匹配排除大小写问题刷新源端和目标端的元数据缓存查引擎日志确认是优化器阶段报错还是执行阶段报错检查函数兼容性不要在所有引擎里使用同样的SQL方言跨引擎联邦查询虽然方便但每引入一个引擎就多一层元数据一致性风险。如果你的BI报表对稳定性要求很高建议尽量把核心数据完整导入OLAP引擎不要长期依赖跨引擎实时关联。5.2 返利状态频繁更新导致的查询结果不准第二个坑是返利状态频繁变化导致的查询结果不准。这个问题一开始出现在我们并行试运行阶段。现象运营看到的当日佣金总额和财务结算系统跑出来的数字差了十几万。排查发现有一部分订单在当天凌晨生成时状态为待结算BI表里已经累计过一次佣金上午用户又申请退款订单取消结算状态变为失效但BI表是Duplicate模型旧版本的明细行没有删除新版本的状态更新又插入了一条新行。这样累计计算时一个订单被算了两次多出来十几万。根因就是数据模型没选对、更新逻辑没做完整。我们最初为了追求查询性能把返利明细表建成了Duplicate模型依赖Flink任务在状态变更时修改订单状态但Duplicate模型本身不支持真正的更新实际是追加写入所以查询结果必然重复。解决方式是改成Aggregate模型或者StarRocks主键表按照订单ID做去重和版本覆盖并在查询SQL中通过状态字段过滤只统计有效状态的行。换到主键表后我们对订单状态字段做了二次校验导入时只允许同一订单ID的最新版本覆盖旧版本彻底解决了重复计算问题。这个坑提醒我们OLAP引擎的更新能力差距不是看文档上的介绍而是要拿自己的业务变更模式去压测。电商领域里“状态频繁流转、历史版本叠加”的场景非常常见选错模型会让报表数据失真比慢查询严重得多。5.3 和ClickHouse对比的一点点补充网上很多人会把Doris、StarRocks和ClickHouse放在一起选型这里简单说说我们当时的对比结论给还在纠结的人一个参考。ClickHouse在单表大聚合场景确实有非常强的扫描性能部署运维也简单如果业务形态是“日志分析、事件分析、单表明细查询”ClickHouse是非常好的选择。但它有几个点在我们场景里不太合适多表join能力相对弱虽然最近版本一直在改进但BI系统星型模型关联查询很多这是个硬伤Update/Delete能力较弱返利状态更新这种需求很难优雅实现对事务语义支持也有限数据一致性和可靠性保障不如Doris/StarRocks反过来ClickHouse的数据压缩率、单表扫描速度以及生态熟悉度在很多团队里是优势。所以我的建议是先看你们业务模型的复杂度。如果业务查询90%以上都是单表聚合、不需要频繁更新ClickHouse值得优先考虑如果像我们这样大量多表关联、有状态更新、还要兼容MySQL协议Doris和StarRocks会更省心。5.4 选型落地后的优化清单最后整理一份我们落地后一直在用的优化清单算是对前面内容的补充收口建表模型优先按业务更新类型选不要默认Duplicate订单明细建议主键表维表用Duplicate即可分桶数不要照抄模板按数据量增长预期和BE数量估算定期检查Tablet大小是否均衡统计信息一定要定期刷新。StarRocks的CBO很依赖统计信息常量更新频繁的大表如果统计信息过期执行计划会退化常见的表现就是join顺序错乱、查询突然变慢慢查询和资源组监控要尽早接入不要等运营反馈才排查。我们上线后第一个月的慢查询日志帮我们发现了很多低效SQL大查询和小查询做好资源隔离宁可小查询慢一点也不要让大查询把集群内存打爆历史数据回填用Broker Load实时数据用Stream Load两套导入策略分开避免互相干扰把引擎换完之后我们BI看板绝大部分报表查询都从十秒以上降到了两秒以内多表join场景也能稳定在五秒内返回。更重要的是返利财务对账的效率提升了一大截运营可以随时看实时佣金数据不再每天等凌晨跑批。我自己在实际操作中最大的体会是选型这件事产品能力文档和社区评价只是参考真正要把自己业务里最难的那几个查询拿到测试环境里用真实数据跑一遍看内存、看延迟、看并发再做决定。Doris和StarRocks没有绝对的高下关键看你业务模型里“更新强度”和“查询复杂度”这两块更偏向哪边。如果团队排查能力一般、又特别依赖社区文档和周边工具Doris的生态会让你少走很多弯路如果复杂查询和并发性能是硬指标StarRocks的表现会更值回票价。最后再分享一个小技巧无论选哪个引擎上线前一定要做一次完整链路的数据一致性校验用一张大表同时从原MySQL和新的OLAP引擎跑同一条聚合SQL做对比看数字是否完全一致。这一步能帮你提前发现很多模型选择和数据清洗层面的隐患等运营和财务找上门再排查就真的晚了。