YON Blog

RelSubset has wrong best cost

最近一个项目用到了 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 不一致。