资讯动态

Calcite物化视图匹配核心:AggregateStarTableRule原理与实战

发布时间:2026/9/9 21:45:14 来源:尧图企业网站定制
很多刚开始接触 Calcite 源码的人看到AggregateStarTableRule这类类名时第一反应往往是哦又一个看不懂的优化规则。但实际上如果你搞懂了这条规则基本就摸清了 Calcite 物化视图匹配和星型模型加速的底层玩法。这个规则藏在org.apache.calcite.plan包附近平时用 Calcite 做数仓、BI 查询加速的同学都会在高并发报表调优时碰到它。本文就带着大家从头梳理它到底解决什么问题、匹配逻辑怎么写、命中后如何改写 RelNode 树以及在真实接入时需要注意哪些细节。1. AggregateStarTableRule 在优化器里的角色它到底在撮合什么1.1 从一个查询优化问题说起假设你有一张销售事实表sales(store_id, product_id, amount)一张门店维度表store(store_id, region, city)一张商品维度表product(product_id, category)。业务上经常想按region和category分组看销售额于是 SQL 大概是SELECT s.region, p.category, SUM(s.amount) FROM sales s JOIN store st ON s.store_id st.store_id JOIN product p ON s.product_id p.product_id GROUP BY s.region, p.category;如果数据量很大GROUP BY加多张大表关联在 Parser 生成的逻辑计划阶段往往是一棵深树。Calcite 的优化器HepPlanner或VolcanoPlanner会应用一堆规则去改变这棵树先下推过滤、再结合投影裁剪、再尝试把Aggregate下推到某个子查询里。但问题来了如果事先在离线层把sales、store、product三张表按region、category维度加总好形成一张预聚合的汇总表也叫汇总表、物化视图或 StarTable 的聚合节点那么这条 SQL 是不是可以直接查这张汇总表而不用现场去GROUP BY几亿行数据AggregateStarTableRule干的就是这件事它尝试识别查询里的Aggregate是否恰好匹配一个已经构建好的星型表中的聚合节点如果匹配就把这次昂贵的聚合计算替换成直接读取预先计算好的结果。换句话说它是 Calcite 里查询改写命中物化视图的核心规则之一。1.2 StarTable 和 Lattice 的关系继续往深处挖AggregateStarTableRule涉及的核心概念是StarTable。StarTable是 Calcite 中Lattice多维数据立方体格的产物。Lattice 描述了一张事实表与若干维度表之间的外键关系像雪花形状一样展开。Calcite 可以通过 Lattice 感知表之间的关联关系并基于这个 星型结构 生成虚拟表StarTable。StarTable内部往往维护了几个Table节点例如事实表和维度表的扫描节点。而针对这个 StarTable 的查询如果在某组维度分组列上做了聚合并且有对应的物化视图/汇总表Calcite 就把这个查询翻译成直接访问更小粒度的汇总表。AggregateStarTableRule就是负责识别并验证这种查询模式的关键规则。可以这么理解StarTable 是维度建模在优化器里的抽象AggregateStarTableRule是把抽象查询落到物理预聚合结果的桥梁。2. 规则源码切入匹配模式、校验条件与核心调度点很多人在看 Calcite 规则源码的时候总觉得matches和onMatch很虚不太清楚每个方法的调用时机。我直接用源码层面来拆解。2.1 类结构概览AggregateStarTableRule在 Calcite 中的定义大致长这样不同版本略有差异核心逻辑不变public class AggregateStarTableRule extends RelOptRule { public static final AggregateStarTableRule INSTANCE new AggregateStarTableRule(operand(Aggregate.class, operand(Project.class, operand(RelOptTableScan.class, none()))), AggregateStarTableRule); public AggregateStarTableRule(RelOptRuleOperand operand, String description) { super(operand, description); } Override public boolean matches(RelOptRuleCall call) { // 输出匹配条件 } Override public void onMatch(RelOptRuleCall call) { // 核心处理 } }注意这里有个陷阱很多同学以为这个规则的匹配模式只有Aggregate - Project - TableScan但实际它的operand写法在不同 Calcite 版本里可能不一样有的版本还会包一层Filter。所以复现时最好先打开自己使用的 Calcite 版本的源码确认一下。2.2 matches 阶段到底校验了什么规则触发后matches方法先做快速过滤。主要做三件事检查最底层的TableScan对应的表是否是StarTable。检查Project如果有是否是简单的映射/别名不包含复杂的表达式。检查Aggregate的分组列是否匹配目标StarTable中的可用维度组合。这就像去店里买套餐matches是看这个订单是不是选中了套餐如果菜品结构完全不是套餐就直接返回 false不会进入后续复杂的匹配逻辑节省优化器的时间。以下是一段示意代码帮助理解Override public boolean matches(RelOptRuleCall call) { final Aggregate aggregate call.rel(0); final Project project call.rel(1); final RelOptTableScan scan call.rel(2); final RelOptTable table scan.getTable(); if (!(table.unwrap(StarTable.class) instanceof StarTable)) { return false; } final StarTable starTable table.unwrap(StarTable.class); // 检查 aggregate 的分组列是否在 starTable 中已定义 return starTable.isValidAggregate(aggregate); }这不是全量源码核心是表达思路。实际源码里还要求aggregate.getGroupSet()不能为空而且aggregate的 agg 调用必须是可在预聚合中计算的。另外如果分组列和度量的组合要求无法从StarTable的某个预聚合结果中得到matches也会返回 false。2.3 onMatch 阶段改写规则的两条路径一旦matches通过onMatch会做最终的重写。这里有一个很重要的分支这张StarTable后面对应的是原始的星型结构即直接扫原始表还是对应到已经物化的表StarTable里通常会有若干个RelNode候选每个候选对应一组预聚合结果。常见的有两种情况如果查询的GROUP BY列正好命中预聚合结果的分组维度那么直接把Aggregate替换成对预聚合表的扫描。如果查询比预聚合结果的粒度更粗例如预聚合结果按region, category分组查询想按region分组那么仍可以基于预聚合结果再做一次聚合即从粗粒度结果进一步聚合。onMatch里的逻辑大致如下Override public void onMatch(RelOptRuleCall call) { final Aggregate aggregate call.rel(0); final Project project call.rel(1); final RelOptTableScan scan call.rel(2); final StarTable starTable scan.getTable().unwrap(StarTable.class); final RelOptTable starRelOptTable starTable.toRel(context - ...); final RelNode substituted starRelOptTable.toRel(scan.getCluster()); // 可能还需要换算列映射、应用 aggregate 到 substituted RelNode newAgg aggregate.copy(aggregate.getTraitSet(), substitute(...)); call.transformTo(newAgg); }这里有两个实现细节值得关注列映射StarTable内部维护了从原始查询列到预聚合表列的映射。在改写时RexBuilder通过RexInputRef将新旧节点连起来把原先分组列的下标换算成新表的下标。这一步很容易出错一不留神就会导致字段错位。聚合函数的可下推性不是所有聚合函数都能从预聚合结果再次聚合得到例如COUNT(DISTINCT x)在低层预聚合时如果没精确去重上层就不能随意复用结果。onMatch内部会检查aggregate调用中的 AggCall 是否被starTable支持否则放弃改写。2.4 触发时机HepPlanner 和 VolcanoPlanner 的差异AggregateStarTableRule既可以被注册到HepPlanner启发式优化器也可以注册到VolcanoPlanner基于代价的优化器。但实际生产中绝大多数用于 Lattice 物化视图匹配的工程实现会把它放到HepPlanner里跑并且严格按照先 expand star table再 aggregate replace的顺序。原因很简单VolcanoPlanner 需要计算代价而 StarTable 匹配是逻辑改写并不需要物理属性。如果塞进 Volcano可能会被其他规则反复触发导致优化过程不可控。所以当你准备在自己的项目里引入这套机制时建议用HepProgramBuilder明确指定规则顺序HepProgramBuilder builder new HepProgramBuilder(); builder.addRuleInstance(AggregateStarTableRule.INSTANCE); // 其他规则... HepPlanner planner new HepPlanner(builder.build());这里还有个小技巧先创建一个Lattice调用addStarTable再注册这个规则才能让 Calcite 知道这个星型结构。3. 规则的实际驱动场景Lattice 物化视图如何与 AggregateStarTableRule 协同3.1 搭建一个 StarTable 需要什么在真实项目里你不大可能直接 newStarTable而是先定义Lattice。看一段典型代码LatticeRootNode rootNode new LatticeRootNode(factTable); rootNode.addChild(ArrayTable.create(...), store, store_id, store_id); rootNode.addChild(ArrayTable.create(...), product, product_id, product_id); Lattice lattice new Lattice.Builder(rootNode) .addMeasure(sum_amount, sales, amount, SUM) .addMeasure(count_sales, sales, sales_id, COUNT) .build();Lattice构建好之后可以借助CalciteSchema的add(star, starTable)把StarTable放到 schema 中。此后用户如果执行一个从 star 表查询 分组的 SQL优化器就会看到这个StarTable并尝试用AggregateStarTableRule改写。这里有一点特别容易混淆Lattice里的StarTable并不是一张真实存在的物理表它更像一个虚拟 SQL 模子。真正可以被查询改写命中的是StarTable中挂载的若干物化视图/汇总表Calcite 中表现为RelOptMaterialization或PolymorphicTable。AggregateStarTableRule负责判定这个 SQL 的聚合是否碰巧和某个物化视图生成的聚合结果一致。如果一致就把它替换成物化视图的表扫描。3.2 从原始查询到预聚合表一个完整映射过程说一个我在实际项目里跑通过的最小示例。假设我创建了sales_star这个 StarTable其中有三个可用的聚合节点节点编号维度组合度量A(region, category)sum(amount)B(region)sum(amount)C(store_id, product_id)sum(amount)当用户查询SELECT region, category, SUM(amount) FROM sales_star GROUP BY region, category时AggregateStarTableRule看到分组列恰好是(region, category)就直接将原先的Aggregate节点替换为对A对应物理表的扫描。如果用户查询SELECT region, SUM(amount) FROM sales_star GROUP BY region理论上可以用 B 直接命中。但如果我只物化了 A更细粒度那么AggregateStarTableRule会怎么处理它不会直接使用 A而会尝试在 A 的结果上再做一次GROUP BY region的聚合即二次聚合。这种情况下生成的计划是Aggregate(region) - Aggregate(region, category) - TableScan(A)。不过这个二次聚合是否能自动生成取决于 Calcite 内部实现版本以及是否配置了aggregate的AggCall可重复聚合例如SUM是可重复聚合的AVG有时必须改为SUM/COUNT。3.3 为什么说这条规则是物化视图匹配的安全门物化视图改写最怕什么怕基于不正确的预聚合数据得到错误结果。比如物化视图里已经按region过滤了一部分数据但查询需要全量region数据一旦错误匹配就会出错。AggregateStarTableRule至少做了三层防御最底层判断只有StarTable上的扫描才能匹配不要试图去匹配任意普通表。分组列验证Aggregate的分组列集合必须是StarTable所定义的某条维度路径的超集或子集完全无关的分组列会被拒绝。聚合函数验证相关 AggCall 必须能在预聚合上通过上卷算子得到比如SUM可以COUNT可以AVG需要转换COUNT(DISTINCT)默认不可直接上卷。正是这些防御让AggregateStarTableRule在 Calcite 优化器里扮演着物化视图安全门的角色。理解这一点之后你在排查为什么我的物化视图没生效时就会先去检查这三点而不是一上来就怀疑规则没注册。4. 手写复现一个最小可运行的 AggregateStarTableRule 示例既然我们要深入理解就不能只看源码得动手搭一个能跑起来的最小环境。这里我用一个简化示例展示如何在项目里注册规则并触发改写。4.1 依赖与模型定义假设你已经在 pom.xml 里引入了org.apache.calcite:calcite-core和org.apache.calcite:calcite-lattice部分版本把 Lattice 移到 core 或单独的模块里请注意版本对应关系。然后定义 SchemaSchemaPlus rootSchema Frameworks.createRootSchema(true); // 创建事实表和维度表 rootSchema.add(sales, new AbstractTable() { Override public RelDataType getRowType(RelDataTypeFactory typeFactory) { return typeFactory.builder() .add(store_id, SqlTypeName.INTEGER) .add(product_id, SqlTypeName.INTEGER) .add(amount, SqlTypeName.DECIMAL) .build(); } }); // ... 其他表接着创建 Lattice并把 StarTable 注册为star_sales。这个过程会涉及一些内部 API实际项目中建议封装成工具类。4.2 配置优化器并触发规则为了验证我们可以直接构造一个逻辑计划或者用 SQL 解析生成FrameworkConfig config Frameworks.newConfigBuilder() .defaultSchema(rootSchema) .build(); Planner planner Frameworks.getPlanner(config); // 假设 SQL select region, category, sum(amount) from star_sales group by region, category但注意很多版本的 Calcite 并不会自动加载 Lattice 相关的规则需要在Planner的Program中显式注册AggregateStarTableRule.INSTANCE。例如Program program Programs.of(RuleSet.of( AggregateStarTableRule.INSTANCE, // 其他需要的规则如 FilterProjectTransposeRule 等 ));然后在优化时调用program.run(...)。如果看到RelNode的逻辑计划从原来的AggregateTableScan(sales)变成了TableScan(aggregated_table)就说明规则成功执行了。4.3 常见不生效原因对照表我把平时排查过的问题整理成一张表供参考现象可能原因处理建议规则没触发StarTable 未正确注册到 Schema检查 Lattice 的addStarTable和 Schema Path规则触发了但转换后报列错误列映射未处理检查 StarTable 的 column mapping 实现分组列对不上查询分组列与物化结果维度不一致确保 Lattice 中 measure 和维度定义覆盖查询列聚合结果偏大/未下推AggCall 无法上卷确认物化表中保存的是SUM/COUNT必要时改查询为SUM(amount)而不是AVG(amount)匹配了但代价反而高物化表行数仍很大增加预聚合结果的粒度或者考虑让规则仅在低基数字段命中这些坑我都实际踩过特别是第一类StarTable注册好但没有设置Lattice导致AggregateStarTableRule的matches永远进不去。5. 工程实战中的改造与调优心得体会光看源码、跑 Demo 还不够真正在业务里要稳定使用还需要根据数据特点做一些定制。5.1 自定义聚合规则处理复杂维度层级AggregateStarTableRule默认只处理简单的SUM/COUNT上卷。如果你的业务里有大量AVG、PERCENTILE等复杂聚合建议不要直接改这条规则那样容易破坏 Calcite 内部的匹配语义。更稳妥的方式是实现一个自定义的RelOptRule参考它的matches/onMatch写法把AggCall转换成内部可恢复的表示。比如对AVG可以先在预聚合层保存SUM和COUNT再在规则里把AVG展开为SUM / COUNTif (aggCall.getAggregation() SqlStdOperatorTable.AVG) { // 创建 sum count 重新组合 }这类似 Calcite 中已有的AggregateExpandDistinctAggregatesRule的做法但使用场景不同。这里要强调的是重写AggCall之后一定要重新检查新生成的Aggregate的分组列顺序否则下游优化器会拿到不一致的RexInputRef。5.2 在规则前先做投影和过滤的规范化AggregateStarTableRule的匹配条件相对严格通常会限制Project是简单的RexInputRef列表。如果查询里有大量表达式例如SELECT region || - || category, SUM(amount) ...那么在调用AggregateStarTableRule之前最好先跑ProjectToWindowRule、ReduceExpressionsRule或者FilterProjectTransposeRule把投影规范化后再交给这个规则。我见过一个线上案例一条 SQL 明明可以用物化表但因为查询里把store_id包装成了CAST(store_id AS BIGINT)导致Project不满足规则要求物化匹配始终失败。后来通过在 Program 里增加一个投影简化规则解决了问题。这个案例说明使用AggregateStarTableRule不是孤军奋战它需要和其他规则形成组合。建议的调用顺序是先将Filter下推Project裁剪。简化表达式。再应用AggregateStarTableRule。最后再做列裁剪和物理优化。5.3 注意版本差异升级时一定要回归Calcite 社区的迭代不算慢AggregateStarTableRule在 1.20 版本前后有过行为调整。比如早期版本要求Aggregate的 groupSet 必须完全等于 StarTable 的维度集合后来放宽为可以是 StarTable 维度集合的子集允许更高层聚合。这些变化直接决定了你的物化表是否命中。我的建议是在升级 Calcite 版本时专门为物化视图改写场景写一组集成测试。每次升级至少跑一遍以下三类 SQL分组列与物化表完全一致。分组列比物化表维度更粗。分组列与物化表维度顺序不同但语义相同。通过这三类用例基本能覆盖AggregateStarTableRule的核心匹配逻辑防止升级带来的隐性行为变化。5.4 与成本模型的取舍最后说一点更深层的体会。AggregateStarTableRule是逻辑改写规则执行后并不保证物理执行计划一定变快。例如物化表的维度组合与查询的分组列维度很接近但是物化表本身数据量仍然很大并且物化表没有针对分组列的索引或排序那么直接扫描原始事实表利用列存和谓词下推可能更快。VolcanoPlanner 下这个规则产出的新节点会和原始节点一起参与代价计算最终选最优但如果是 HepPlanner 场景你需要非常小心因为一旦调用transformTo它可能会立即替换掉原来的节点没有代价比较。所以如果你的查询模式比较复杂建议在 Hep 阶段先不急于做 StarTable 替换而是用AbstractConverter或自定义代价函数控制。我在生产环境中的做法是将AggregateStarTableRule注册到 VolcanoPlanner并给预聚合表设置准确的rowCount和cpu代价这样优化器才能做出正确取舍。如果拿不到统计信息宁可不启用这条规则也不要盲目匹配。写在最后AggregateStarTableRule是 Calcite 优化器里把维度建模和逻辑改写连接起来的关键节点。它看似只是一个小规则背后却牵扯到 Lattice、StarTable、物化视图、聚合上卷等多个机制。如果你正在做基于 Calcite 的查询加速引擎我强烈建议你从Lattice开始逐步把事实表、维度表、预聚合表串起来再回头阅读这个规则的源码你会发现很多之前觉得晦涩的接口突然就通了。我个人在实际项目里体会最深的一点是规则代码本身不难难的是配套的元数据管理和代价校准。先把维度层级定义清楚把预聚合表的统计信息喂准AggregateStarTableRule才能真正发挥出它一个规则盘活一座星型模型的价值。如果你只是浅尝辄止跑个 Demo可能会觉得它没什么用但如果你把它放进一个真实的高并发报表系统里并被它优化过几次秒级出结果的查询你一定会忍不住吹爆它。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价