Apache Calcite SQL 优化器
什么是 Apache Calcite SQL 优化器
Apache Calcite 是一个动态数据管理框架,其核心是强大的 SQL 解析、验证、优化与执行引擎。
与传统数据库内置的优化器不同,Calcite 的优化器可以脱离具体存储与计算引擎独立使用,它不存储数据,只负责 将逻辑查询计划转换为高效的物理执行计划。
优化器接收经过验证的 逻辑关系代数树(RelNode 树),应用一系列重写规则(Rules),最终输出一个被优化后的等价计划。这一过程基于两大基石:关系代数 与 成本模型,并以 Volcano/Optimizer 作为计划搜索框架。
核心概念:优化的三要素
1. 逻辑计划与物理计划
- 逻辑计划(Logical Plan):描述“做什么”,例如
LogicalProject、LogicalFilter、LogicalJoin。它与数据存储方式和执行算法无关。 - 物理计划(Physical Plan):描述“怎么做”,例如
NestedLoopJoin、HashJoin、SortMergeJoin。它指定了具体的算法实现。
优化器的任务就是将一个逻辑计划映射到某个最优(或成本最小)的物理计划。
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替换为EnumerableHashJoin或EnumerableNestedLoopJoin。
Metadata Providers(元数据提供者)
优化需要估算中间结果的行数、选择率、数据大小等,这些信息由元数据系统提供。开发者可自定义统计信息源,但 Calcite 默认会基于主键、唯一键等启发式估算。
Cost Model(成本模型)
成本通过 RelOptCost 接口定义,通常包含 CPU、IO、行数 等维度。不同 Convention 可定义自己的 RelOptCostFactory。优化器递归计算备选计划的累计成本,并剪枝高成本分支。
优化流程详解(六步走)
假设收到一个简单的 SQL:SELECT deptno, COUNT(*) FROM emp WHERE sal>1000 GROUP BY deptno。
- 解析(Parsing):SQL 文本 → SqlNode AST。
- 验证(Validation):SqlNode 结合元数据(表结构、字段类型)解析为
SqlValidatedNode。 - 逻辑计划生成:
SqlToRelConverter将验证后的节点转为纯逻辑RelNode树。 - 逻辑优化(Rewrite):应用大量 转换规则,例如常量折叠、谓词下推、列裁剪、子查询消除等,生成经过重写的逻辑计划。
- 物理优化(Optimize):调用优化器核心
VolcanoPlanner.findBestExp()。- 将逻辑节点的 Convention 标记为
NONE,目标 Convention 设为所需(如ENUMERABLE)。 - 优化器内部维护一个等价节点集合。应用物理实现规则,产生该 Convention 的物理节点。
- 满足排序、分布等 Trait 要求,并计算每条路径的成本。
- 根据最小成本选出最优物理计划。
- 将逻辑节点的 Convention 标记为
- 代码生成/执行:最优计划交给后续的
EnumerableRel实现,生成 Java 代码(Janino 实时编译)并迭代执行。
谓词下推与列裁剪(常见逻辑优化)
谓词下推(Predicate Pushdown)
FilterTableScanRule 或 FilterJoinRule 等规则将过滤条件尽可能推向数据源或靠近扫描节点。例如:
- 将
WHERE sal>1000推进到LogicalTableScan中,甚至在适配器中转换为底层数据源的过滤请求。 - 在 Join 前,将只涉及单表的谓词推到对应表的扫描之上,减少 Join 的输入数据量。
列裁剪(Column Pruning)
ProjectMergeRule 和 ProjectRemoveRule 等,只保留查询实际需要的列。如果 emp 表有 20 列,但查询只用到 deptno 和 sal,则 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 中的 getRowCount、getSelectivity 方法,使优化器获得更真实的基数估计。
调试与跟踪优化过程
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.plan和org.apache.calcite.optimizer设置为 DEBUG 级别,输出规则匹配、成本比较等详细信息。
常见问题与误区
- 为什么我的自定义规则没有触发?
检查 operand 匹配模式是否正确,逻辑节点的 Convention 是否为
NONE(或者你期待的 Convention)。物理实现规则需要逻辑节点 Convention 为NONE。 - 优化器性能过慢
确保没有使用过多无谓的规则;对于复杂查询,Volcano 搜索空间会爆炸。此时应通过
VolcanoPlanner的setTopDownOpt或启用 Top-down 优化器(Cascades) 改进性能,或者限制搜索时间(setTimeoutMillis)。 - 物理计划未按预期选择 成本函数可能需要校准。可通过打印各物理节点的成本来诊断,也可能是缺少特定 Trait 满足规则(例如缺少排序时无法选择 SortMergeJoin)。
进阶学习路径
- 阅读源码包
core/src/main/java/org/apache/calcite/plan/volcano/理解 Volcano 内核。 - 研究
RelOptRule的成熟实现,例如FilterJoinRule、ReduceExpressionsRule。 - 学习自定义 Convention 与物理节点,实现完整的 Adapter。
- 掌握
RelTraitDef与规划器 Trait 的注册,使优化器能够处理物理属性请求。
Apache Calcite 的优化器是你构建高性能数据处理系统的强力工具,掌握其原理和定制方法,将使你能在多种场景下获得最优的 SQL 执行计划。