4、StarRocks核心原理:CBO优化器、Pipeline执行引擎、主键模型与更新模型

StarRocks 能跑这么快,靠的不是单一绝招。我接触过不少 OLAP 系统,有的优化器强但执行引擎弱,有的存储好但查询慢。StarRocks 在「优化器 + 执行引擎 + 存储模型」三个层面都下了功夫。今天咱们就拆开看看,这几个核心部件到底是怎么工作的。

4.1 CBO 优化器:查询的「导航仪」

CBO,全称 Cost-Based Optimizer,基于成本的优化器。说白了,就是 SQL 进来之后,系统得决定「怎么查最快」。是先用过滤条件?还是先做 Join?用 Hash Join 还是 Broadcast Join?

我刚开始做数仓时,见过不少系统用 RBO(Rule-Based Optimizer),就是写死一套规则:先过滤、再 Join、再聚合。但规则是死的,数据是活的。一张 1 亿行的表和一张 100 行的表做 Join,执行顺序不同,性能差几十倍。

CBO 的核心思路:不是按固定规则走,而是枚举多种执行计划,估算每种计划的「代价」(CPU、内存、网络、IO),选代价最小的那个。

StarRocks 的 CBO 做了几件很实在的事:

  • 统计信息收集:自动收集表的行数、列的唯一值数量、NULL 值比例、数据分布直方图。没有这些,成本估算就是瞎猜。
  • Join 重排:多表 Join 时,自动决定哪张表做驱动表、用哪种 Join 算法。我遇到过一张大表和一张维表 Join,CBO 自动选择了 Colocate Join,数据不用跨节点传输,查询从 5 秒降到 0.3 秒。
  • 谓词下推:把 WHERE 条件尽量推到数据扫描阶段,越早过滤掉无关数据越好。

个人经验:CBO 不是万能的。如果统计信息过期(比如表数据量变了但没做 ANALYZE),CBO 可能选错计划。我习惯在数据批量导入后,手动执行 ANALYZE TABLE 更新统计信息。

4.2 Pipeline 执行引擎:让 CPU 不再「空转」

传统的火山模型(Volcano Model)是「拉取式」的:每个算子从下游拉一行数据,处理完再往上吐。这样做的好处是简单,但坏处也很明显——每处理一行就要做一次虚函数调用,CPU 的指令缓存和分支预测被频繁打乱。

StarRocks 的 Pipeline 执行引擎换了个思路:把 SQL 拆成多个 Pipeline,每个 Pipeline 内部用「数据驱动」的方式执行。

举个例子,一个查询包含 Scan、Filter、Aggregate 三个算子。传统模型是:Scan 吐一行 → Filter 处理一行 → Aggregate 处理一行。Pipeline 模型是:Scan 一次性吐一批数据(比如 4096 行),Filter 处理完这批再往下传,Aggregate 拿到一批数据后批量计算。

Pipeline 的好处:

  • 减少虚函数调用,提升 CPU 利用率
  • 支持算子间的并行执行(不同 Pipeline 可以在不同线程上跑)
  • 天然适配多核 CPU,充分利用硬件资源

我记得有一次调优一个慢查询,执行计划显示 Scan 阶段只用了单线程。后来发现是表的分区数太少,导致 Pipeline 无法拆分。调整分区策略后,查询并行度从 1 提升到 8,耗时从 12 秒降到 2 秒。

注意:Pipeline 执行引擎对内存有一定要求。如果并发查询太多,每个 Pipeline 都申请一批内存,可能导致 OOM。我建议根据集群规模合理设置 pipeline_dop(并行度参数),不要盲目开大。

4.3 主键模型与更新模型:实时更新的「杀手锏」

说到 StarRocks 的实时更新能力,就绕不开主键模型(Primary Key Model)和更新模型(Update Model)。这两个模型解决的是同一个问题:如何在 OLAP 场景下高效地做数据更新。

传统做法是用 Merge-On-Read:写入时只追加,查询时合并多条记录。这样写入快,但查询慢(因为要合并)。StarRocks 的主键模型反其道而行之——写入时就做去重和合并,查询时直接读最终结果。

特性 主键模型 (Primary Key) 更新模型 (Update)
写入方式 UPSERT(存在即更新,不存在即插入) UPSERT
查询性能 极高(无需合并) 高(少量合并)
存储开销 较高(需要维护主键索引) 较低
适用场景 实时订单、金融交易、需要精确去重 实时报表、轻度更新的维度表
主键去重时机 写入时(立即合并) 查询时(合并少量版本)

主键模型的核心是「主键索引」。StarRocks 把主键索引存在内存中,写入时通过索引快速定位到目标行,直接覆盖旧数据。这样查询时就不需要做任何合并操作。

但这里有个坑:主键索引占内存。如果主键字段很长(比如 UUID 字符串),索引会吃掉大量内存。我遇到过一张 10 亿行的表,主键是 32 位字符串,光索引就占了 30GB 内存。

避坑指南:我曾经在一个实时订单场景中,把订单 ID(32 位 UUID)设为主键。结果集群内存很快被打满。后来我把主键改为「日期 + 自增 ID」的组合,长度从 32 字节降到 8 字节,内存占用直接降了 75%。

更新模型则更轻量。它允许主键重复,写入时只追加新版本,查询时通过版本号取最新的一条。这样做写入更快,但查询时需要做一次「取最新版本」的操作。对于更新频率不高的场景(比如每天更新一次的维度表),更新模型完全够用。

4.4 三者的协同:一个查询的生命周期

咱们把 CBO、Pipeline 和存储模型串起来,看一个查询是怎么跑的:

  1. SQL 解析:语法解析、语义检查
  2. CBO 优化:生成多个执行计划,估算成本,选最优
  3. 计划拆分:把执行计划拆成多个 Pipeline
  4. 数据扫描:根据存储模型(主键/更新/明细),从磁盘读取数据
  5. Pipeline 执行:各 Pipeline 并行执行,算子间批量传递数据
  6. 结果汇总:合并各节点的中间结果,返回给客户端

你看,每一步都环环相扣。CBO 决定了「走哪条路」,Pipeline 决定了「怎么跑得快」,存储模型决定了「数据怎么存和怎么更新」。缺一个,性能都上不去。

我的建议:

  • 如果业务对实时更新要求高(秒级),优先选主键模型
  • 如果更新频率低(分钟级或小时级),更新模型更省资源
  • 如果只做追加写入(日志、事件),用明细模型就行,别浪费内存建索引
StarRocks 核心原理架构图 StarRocks 核心原理架构 SQL 查询 CBO 优化器 统计信息 · Join 重排 · 谓词下推 Pipeline 执行引擎 数据驱动 · 批量处理 · 多核并行 主键模型 写入即合并 · 内存索引 更新模型 追加写入 · 查询合并 明细模型 只追加 · 无去重 查询结果 说明: CBO 决定「走哪条路」,Pipeline 决定「怎么跑得快」,存储模型决定「数据怎么存和怎么更新」。 三者协同工作,缺一不可。实际调优时,需要根据业务场景选择合适的存储模型。

嗯,这张图把今天讲的内容串起来了。你想想看,一个查询从 SQL 进来,经过 CBO 优化、Pipeline 执行、存储模型读取,最后返回结果。每一步都有它的设计哲学,也都有它的适用边界。

我个人觉得,理解这些原理比背参数更重要。参数可以查文档,但原理决定了你在遇到问题时能不能快速定位根因。下次咱们聊数据建模时,你会发现这些原理会反复出现——因为建模方案的选择,本质上就是在利用这些底层机制。