RelSubset has wrong best cost
02 Nov 2022最近一个项目用到了 Apache 的 Calcite 来做 SQL 优化,今天运行的时候发现报下面的错误:
java.lang.AssertionError: RelSubset [rel#29:RelSubset#3.ENUMERABLE.[]] has wrong best cost {55.166666666666664 rows, 152.92333333333335 cpu, 0.0 io}. Correct cost is {150.33333333333334 rows, 150.33666666666667 cpu, 0.0 io}
Debug 了一下发现这个报错其实是可以忽略的,只要把日志级别调高到 INFO 及以上即可。相关代码是:
// org.apache.calcite.plan.volcano.VolcanoPlanner#ensureRegistered
// Checking if tree is valid considerably slows down planning
// Only doing it if logger level is debug or finer
if (LOGGER.isDebugEnabled()) {
assert isValid(Litmus.THROW);
}
但是,这个错误的根本原因是什么呢?从字面意思来理解就是 RelSubset 的 best cost 错误,再结合代码来看,报错的真正意思是,某个 RelSubset 之前计算的 best cost 和基于 best 的节点重新计算的 cost 不一致:
// org.apache.calcite.plan.volcano.VolcanoPlanner#isValid
// Make sure bestCost is up-to-date
try {
RelOptCost bestCost = getCostOrInfinite(subset.best, metaQuery);
if (!subset.bestCost.equals(bestCost)) {
return litmus.fail("RelSubset [" + subset
+ "] has wrong best cost "
+ subset.bestCost + ". Correct cost is " + bestCost);
}
} catch (CyclicMetadataException e) {
// ignore
}
下一个问题就是,为什么会发生这种情况呢?根源还是因为 Calcite 优化时是寻找的局部最优策略,某些情况下会出现某个 RelSubset 的 best cost 下降,但是以该 RelSubset 为 input 的 RelSubset 的 best cost 反而会上升,这样 best cost 就不会更新,更新 best cost 的代码是:
// org.apache.calcite.plan.volcano.VolcanoPlanner#propagateCostImprovements
while ((relNode = propagateHeap.poll()) != null) {
RelOptCost cost = requireNonNull(propagateRels.get(relNode), "propagateRels.get(relNode)");
for (RelSubset subset : getSubsetNonNull(relNode).set.subsets) {
if (!relNode.getTraitSet().satisfies(subset.getTraitSet())) {
continue;
}
if (!cost.isLt(subset.bestCost)) {
// 新计算的 cost 大于之前的 best cost,不更新 best cost
continue;
}
// 省略
}
}
我这边的情况是 EnumerableProject 节点的导致的,虽然它的 input 的 RelSubset 的 best cost 下降了,但是其输出的 Row 的数量变多了,导致了 EnumerableProject 节点自身的 cost 上升,这就造成了以 EnumerableProject 为 best 的 RelSubset 的旧 best cost 和基于 EnumerableProject 节点重新计算的 best cost 不一致。