Apache Calcite SQL 优化器

FreeGuideOnline 最新 2026-07-13

什么是 Apache Calcite SQL 优化器

Apache Calcite 是一个动态数据管理框架,其核心是强大的 SQL 解析、验证、优化与执行引擎
与传统数据库内置的优化器不同,Calcite 的优化器可以脱离具体存储与计算引擎独立使用,它不存储数据,只负责 将逻辑查询计划转换为高效的物理执行计划

优化器接收经过验证的 逻辑关系代数树(RelNode 树),应用一系列重写规则(Rules),最终输出一个被优化后的等价计划。这一过程基于两大基石:关系代数成本模型,并以 Volcano/Optimizer 作为计划搜索框架。


核心概念:优化的三要素

1. 逻辑计划与物理计划

  • 逻辑计划(Logical Plan):描述“做什么”,例如 LogicalProjectLogicalFilterLogicalJoin。它与数据存储方式和执行算法无关。
  • 物理计划(Physical Plan):描述“怎么做”,例如 NestedLoopJoinHashJoinSortMergeJoin。它指定了具体的算法实现。

优化器的任务就是将一个逻辑计划映射到某个最优(或成本最小)的物理计划。

2. 关系表达式(RelNode)

RelNode 是 Calcite 中所有计划节点的抽象基类。每一个操作(扫描、过滤、投影、连接等)都对应一个 RelNode 子类。优化规则通过这些节点匹配和替换子树来重写计划。

3. Planner 与 Trait(特征)

Calcite 将计划所需满足的物理属性抽象为 Trait,常见的 Trait 有:

  • Convention(约定):表示节点属于哪种执行模型,例如 EnumerableConvention(内置迭代器模型)、JdbcConvention(JDBC 下行)等。这是最核心的 Trait。
  • RelCollation(排序):数据按哪些列排序。
  • RelDistribution(分布):数据如何在节点间分布(单例、哈希、范围等)。

优化器通过将逻辑计划的 Convention 从 NONE 转换为目标 Convention,从而找到一条物理计划路径。


Volcano 优化器的内部工作流

Calcite 采用 Volcano 优化器(也称基于成本的优化器),它使用 动态规划 搜索等价计划空间。主要组件包括:

Rules(规则)

优化规则是 RelOptRule 的子类,定义 onMatch 方法,当发现可匹配的子树时,返回一个等价的、更优的 RelNode 子树。规则分为两类:

  • 转换规则(Transformation Rule):逻辑结构变化,如 FilterJoinRule 将 Filter 下推到 Join 下方,JoinToLogicalCorrelate 将 Join 转为关联子查询。
  • 物理实现规则(Implementation Rule):逻辑节点替换为物理节点,如 LogicalJoin 替换为 EnumerableHashJoinEnumerableNestedLoopJoin

Metadata Providers(元数据提供者)

优化需要估算中间结果的行数、选择率、数据大小等,这些信息由元数据系统提供。开发者可自定义统计信息源,但 Calcite 默认会基于主键、唯一键等启发式估算。

Cost Model(成本模型)

成本通过 RelOptCost 接口定义,通常包含 CPU、IO、行数 等维度。不同 Convention 可定义自己的 RelOptCostFactory。优化器递归计算备选计划的累计成本,并剪枝高成本分支。


优化流程详解(六步走)

假设收到一个简单的 SQL:SELECT deptno, COUNT(*) FROM emp WHERE sal>1000 GROUP BY deptno

  1. 解析(Parsing):SQL 文本 → SqlNode AST。
  2. 验证(Validation):SqlNode 结合元数据(表结构、字段类型)解析为 SqlValidatedNode
  3. 逻辑计划生成SqlToRelConverter 将验证后的节点转为纯逻辑 RelNode 树。
  4. 逻辑优化(Rewrite):应用大量 转换规则,例如常量折叠、谓词下推、列裁剪、子查询消除等,生成经过重写的逻辑计划。
  5. 物理优化(Optimize):调用优化器核心 VolcanoPlanner.findBestExp()
    • 将逻辑节点的 Convention 标记为 NONE,目标 Convention 设为所需(如 ENUMERABLE)。
    • 优化器内部维护一个等价节点集合。应用物理实现规则,产生该 Convention 的物理节点。
    • 满足排序、分布等 Trait 要求,并计算每条路径的成本。
    • 根据最小成本选出最优物理计划。
  6. 代码生成/执行:最优计划交给后续的 EnumerableRel 实现,生成 Java 代码(Janino 实时编译)并迭代执行。

谓词下推与列裁剪(常见逻辑优化)

谓词下推(Predicate Pushdown)

FilterTableScanRuleFilterJoinRule 等规则将过滤条件尽可能推向数据源或靠近扫描节点。例如:

  • WHERE sal>1000 推进到 LogicalTableScan 中,甚至在适配器中转换为底层数据源的过滤请求。
  • 在 Join 前,将只涉及单表的谓词推到对应表的扫描之上,减少 Join 的输入数据量。

列裁剪(Column Pruning)

ProjectMergeRuleProjectRemoveRule 等,只保留查询实际需要的列。如果 emp 表有 20 列,但查询只用到 deptnosal,则 TableScan 只会请求这两列,大幅减少 IO。


自定义优化规则的步骤

当内置规则无法满足特定业务逻辑时,需自定义规则。 以下是一个简单的自定义规则框架:

public class MyFilterPhysicalRule extends RelRule<MyFilterPhysicalRule.Config> {
  public MyFilterPhysicalRule(Config config) {
    super(config);
  }
  
  @Override
  public void onMatch(RelOptRuleCall call) {
    final LogicalFilter filter = call.rel(0);
    final RelNode input = call.rel(1);
    // 将 LogicalFilter + input 替换为自己的物理节点
    RelNode physNode = new MyPhysicalFilterNode(
        filter.getCluster(),
        input,
        filter.getCondition()
    );
    call.transformTo(physNode);
  }
  
  public interface Config extends RelRule.Config {
    Config DEFAULT = EMPTY_CONFIG
        .withOperandSupplier(b0 ->
            b0.operand(LogicalFilter.class).anyInputs())
        .as(Config.class);
    
    @Override default MyFilterPhysicalRule toRule() {
      return new MyFilterPhysicalRule(this);
    }
  }
}

关键点

  • 通过 withOperandSupplier 定义规则匹配的模式。
  • onMatch 中调用 call.transformTo() 提交等价转换。
  • 注册规则: planner.addRule(MyFilterPhysicalRule.Config.DEFAULT.toRule());

成本模型的定制化

若默认成本估算不够精准(例如你的存储引擎能提供更精确的统计),可以实现自定义的 RelMetadataQuery 或直接提供 RelOptCost 工厂。

简单定制成本工厂示例:

RelOptCostFactory costFactory = new RelOptCostFactory() {
  public RelOptCost makeCost(double rowCount, double cpu, double io) {
    return new RelOptCostImpl(rowCount, cpu, io);
  }
  // 实现 makeHugeCost, makeInfiniteCost 等
};
planner.setCostFactory(costFactory);

更常见的是覆盖 RelMetadataQuery 中的 getRowCountgetSelectivity 方法,使优化器获得更真实的基数估计。


调试与跟踪优化过程

Calcite 提供了丰富的 Hook 和日志来观察优化。

  • 设置 Planner 的 listener:可记录每次规则调用和计划的变换。
    planner.addListener(new RelOptListener() {
      public void relEquivalenceFound(RelEquivalenceEvent event) { /*...*/ }
      public void ruleAttempted(RuleAttemptedEvent event) { /*...*/ }
      //...
    });
    
  • 打印计划:在优化前后调用 RelOptUtil.toString(relNode) 查看树结构。
  • 启用 DEBUG 日志:在 log4j 配置中将 org.apache.calcite.planorg.apache.calcite.optimizer 设置为 DEBUG 级别,输出规则匹配、成本比较等详细信息。

常见问题与误区

  • 为什么我的自定义规则没有触发? 检查 operand 匹配模式是否正确,逻辑节点的 Convention 是否为 NONE(或者你期待的 Convention)。物理实现规则需要逻辑节点 Convention 为 NONE
  • 优化器性能过慢 确保没有使用过多无谓的规则;对于复杂查询,Volcano 搜索空间会爆炸。此时应通过 VolcanoPlannersetTopDownOpt 或启用 Top-down 优化器(Cascades) 改进性能,或者限制搜索时间(setTimeoutMillis)。
  • 物理计划未按预期选择 成本函数可能需要校准。可通过打印各物理节点的成本来诊断,也可能是缺少特定 Trait 满足规则(例如缺少排序时无法选择 SortMergeJoin)。

进阶学习路径

  • 阅读源码包 core/src/main/java/org/apache/calcite/plan/volcano/ 理解 Volcano 内核。
  • 研究 RelOptRule 的成熟实现,例如 FilterJoinRuleReduceExpressionsRule
  • 学习自定义 Convention 与物理节点,实现完整的 Adapter。
  • 掌握 RelTraitDef 与规划器 Trait 的注册,使优化器能够处理物理属性请求。

Apache Calcite 的优化器是你构建高性能数据处理系统的强力工具,掌握其原理和定制方法,将使你能在多种场景下获得最优的 SQL 执行计划。