Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CALCITE-3916] Support top-down rule apply and upper bound space pruning #1991

Closed
wants to merge 11 commits into from

Conversation

FatLittle
Copy link

related to #1950

  1. Introduce Top-Down rule applying strategy in VolcanoPlanner
  2. Implement tasks and state transformations according to the design of Columnbia
  3. Introduce a LowerBoundCost metadata to compute lower bound and used for pruning
  4. Using a flag to control whether to use new strategy or not

@@ -266,15 +266,14 @@ public static Program standard() {
public static Program standard(RelMetadataProvider metadataProvider) {
final Program program1 =
(planner, rel, requiredOutputTraits, materializations, lattices) -> {
planner.setRoot(rel);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting root before addMaterialization will make MaterializedViewRule always fail to match

@@ -378,14 +378,14 @@ where exists (select 1 from "hr"."emps" where "empid" < 0);
(0 rows)

!ok
EnumerableCalc(expr#0..1=[{inputs}], deptno=[$t0])
EnumerableCalc(expr#0..1=[{inputs}], deptno=[$t1])
EnumerableNestedLoopJoin(condition=[true], joinType=[inner])
Copy link
Author

@FatLittle FatLittle May 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some join factors are swapped because when rule applying order changes, the id of join factors also changed. And EnumerableNestedLoopJoin has a special handling that it prefers left branch having a lower id than the right branch.

@@ -73,6 +74,7 @@

@Test void testFilterWithProject() {
assertModel(MODEL)
.with(CalciteConnectionProperty.TOP_DOWN_OPT.camelName(), false)
Copy link
Author

@FatLittle FatLittle May 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skip using top-down optimization here. This case will produce another plan with the same cost as the expected plan
(Two candidates are both in the memo but the new best plan is produced first). But it will hang when executing the new plan. Still don't know why it hangs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@XuQianJin-Stars Do you have any insight?

@hsyuan hsyuan changed the title [CALCITE-3916] Implement top-down rule applying and upper bound space pruning in calcite [CALCITE-3916] Support top-down rule apply and upper bound space pruning May 29, 2020
clearProcessed(planner.getSet(parentRel));
}
if (subset == planner.root) {
tasks.push(new OptimizeGroup(subset, planner.infCost));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand right, when two sets merged together the optimization task will be restarted from the root, i.e. from the very beginning. Am I right? If so, shouldn't the tasks queue be cleaned in that case? Or have I missed something here?

Copy link
Author

@FatLittle FatLittle Jun 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task queue cannot be cleared here, because RuleMatchs are already popped from rule queue during ApplyRules. So ApplyRule tasks in the queue cannot be reproduced after cleaning.

The newly added task may rescan the root RelSet and schedule tasks for new relnodes. Duplicate tasks can be avoided as input RelSubsets still hold the previous optimizing status.

The only problem is that it may affect the rule applying sequence.

@FatLittle FatLittle force-pushed the odps-cascade-one-planner branch 2 times, most recently from ef9dae4 to f21f99d Compare June 2, 2020 04:42

public MetadataDef<LowerBoundCost> getDef() {
return BuiltInMetadata.LowerBoundCost.DEF;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't looks like suitable for metadata. Why not just integrate it with planner, and let it overridable?

Copy link
Author

@FatLittle FatLittle Jun 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will, it is somewhat like a metadata.

  1. Different subclass of RelNodes may have different logic, like AC, RelNode, SpoolNode, RelSubset ...
  2. It needs a cache and the cache should be invalidated when its input is changed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently every time we add a new relnode through rule transformation, the whole metadata cache will be invalidated, which is not helpful for caching lower bound value.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After CALCITE-2018 is resolved, I think the invalidation of metadata could be removed.

@@ -179,6 +180,7 @@

@Test void testPushDownSort() {
CalciteAssert.model(JdbcTest.SCOTT_MODEL)
.with(CalciteConnectionProperty.TOPDOWN_OPT.camelName(), false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, the new planner is producing a plan with exactly the same cost.
However, the new plan will affect later tests' assertion. I didn't want to modified so many of them. So I added this line instead of modifying the expects.

if (needsConverter && !planner.topDownOpt) {
addConverters(subset, required, true);
if (needsConverter) {
addConverters(subset, required, !planner.topDownOpt);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, that means whenever a new traitset is added, it will always add the enforcer if needed.

Copy link
Author

@FatLittle FatLittle Jun 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. There are reasons for adding converters eagerly:

  1. The converters act as bridges between the working subset and other subsets. Currently, the OptimizeGroup task will schedule tasks for all the logical Rels in the RelSet while it skips physical Rels from other subsets. That's because we are confident that if other subset can be reached by a converter, the converter is already in the current working subset.
  2. In tasks other than OptimizeGroup, when new physical Rels are added to the RelSet, the insertion of the converter is a signal for RuleDriver to schedule the optimizing tasks for the previous working subset.
  3. The computation of lower bound requires the memo to be connected.

Maybe there are some other ways to achieve all this, but I cannot find a more pretty way now. I was thinking of checking the upper bound before adding the enforcer. But I cannot decide which place is suitable for adding the enforcer back when the upper bound is changed. So I leave it for further optimizations.

/**
* RelNode ids that is invoked passThrough method before
*/
Set<Integer> passThroughCache;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use RelNode here, instead of rel id, for debug-ability.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Point. Will resolve it.

/**
* A rule queue that manage rule matches for cascade planner
*/
public class CascadeRuleQueue implements RuleQueue {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a rule queue for top-down? Can't it just execute after matching?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The origin cascades planner fire rules by required. But in this design, rules are fired during registering and executed by required.
We are trying to use on-demand rule firing. But it requires more controls to avoid miss-trigger or re-trigger rules. Related works are still under testings

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will create another JIRA to support on-demand rule matching.

matchList.clear();
}
}
public interface RuleQueue {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's remove public.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

* <p>In each phase, the planner then iterates over the rule matches presented
* by the rule queue until the rule queue becomes empty.
*/
public class MultiPhasedRuleDriver implements RuleDriver {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MultiPhasedRuleDriver -> IterativeRuleDriver
The multiphase doesn't work well at all. No body use it, we should consider removing multi phases in volcano planner.
and remove public

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved


private boolean isLogical(RelNode relNode) {
return relNode.getTraitSet().getTrait(ConventionTraitDef.INSTANCE)
== Convention.NONE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may be other usages in down stream project that defines their own logical conventions, while still keep NONE. That means they are using 2 logical conventions......
But let's keep it simple for NONE for now.

Copy link
Author

@FatLittle FatLittle Jun 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, it's my typo. I should use planner.isLogical instead of a new method here. planner.isLogical is supposed to be overwritten by users.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved


public RelOptCost getLowerBoundCost(AbstractConverter ac,
RelMetadataQuery mq, RelOptPlanner planner) {
return mq.getLowerBoundCost(ac.getInput(), planner);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still use abstract converter?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it is not possible to have AC here. But as a metadata handler,I tried to make its logic as complete as possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, once you add it, it will be hard to remove it and will always maintain it. Let's remove it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

@FatLittle FatLittle force-pushed the odps-cascade-one-planner branch 3 times, most recently from 4c2f0f3 to 62e2f79 Compare June 11, 2020 12:46
} catch (VolcanoTimeoutException e) {
planner.canonize();
ruleQueue.phaseCompleted(phase);
break PLANNING;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does call drive recursively here yield the same semantic ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is completely copied from previous findBestExpr method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zinking break PLANNING; here means step out of the for loop. It is not a recursive call.

+ " EnumerableCalc(expr#0..27=[{inputs}], expr#28=['1998Q1'], expr#29=[=($t15, $t28)], expr#30=['1998Q2'], expr#31=[=($t15, $t30)], expr#32=['1998Q3'], expr#33=[=($t15, $t32)], expr#34=[OR($t29, $t31, $t33)], proj#0..27=[{exprs}], $condition=[$t34]): rowcount = 18262.25, cumulative cost = {91311.25 rows, 4748186.0 cpu, 0.0 io}\n"
+ " EnumerableTableScan(table=[[TPCDS, DATE_DIM]]): rowcount = 73049.0, cumulative cost = {73049.0 rows, 73050.0 cpu, 0.0 io}\n"
+ " EnumerableTableScan(table=[[TPCDS, STORE_RETURNS]]): rowcount = 287514.0, cumulative cost = {287514.0 rows, 287515.0 cpu, 0.0 io}\n"
+ " EnumerableSort(sort0=[$31], sort1=[$43], dir0=[ASC], dir1=[ASC]): rowcount = 3.94888649445E9, cumulative cost = {7.900926597146687E9 rows, 2.163983100662313E13 cpu, 0.0 io}\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't derive that these two are equivalent, how did you guys do that ?

Copy link
Author

@FatLittle FatLittle Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only changes is that some HashJoin is changed to MergeJoin. But as you can see from the cost, the new plan do have a lower cost.
(
old cost = 1.2435775409784036E28
VS
new cost = 1.2430341380834431E28
)

/**
* RelNode ids that is invoked passThrough method before
*/
Set<RelNode> passThroughCache;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the comment too~

Copy link
Author

@FatLittle FatLittle Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I think it is better to store the passThroughCache to RelSet, where the key is the traitset, the value are relnodes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no differences. Currently, RelSet + Traits = RelSubset

+ " CassandraLimit(fetch=[2])\n"
+ " CassandraProject(tweet_id=[$2])\n"
+ " CassandraProject(tweet_id=[$2])\n"
+ " CassandraLimit(fetch=[2])\n"
+ " CassandraFilter(condition=[=($0, '!PUBLIC!')])\n");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the same cost lead to the change?

Copy link
Author

@FatLittle FatLittle Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the have the same cost. However, I do think in most case the new plan is better.

EnumerableAggregate(group=[{2, 3, 7}], groups=[[{2, 3, 7}, {3}]], EXPR$2=[MIN($5)], EXPR$3=[MAX($5)], $g=[GROUPING($3, $7, $2)])
EnumerableAggregate(group=[{1}], EXPR$1=[COUNT($2, $0) FILTER $5], EXPR$2=[MIN($3) FILTER $6], EXPR$3=[MIN($4) FILTER $6])
EnumerableCalc(expr#0..5=[{inputs}], expr#6=[0], expr#7=[=($t5, $t6)], expr#8=[5], expr#9=[=($t5, $t8)], proj#0..4=[{exprs}], $g_0=[$t7], $g_5=[$t9])
EnumerableAggregate(group=[{2, 3, 7}], groups=[[{2, 3, 7}, {3}]], EXPR$2=[MIN($5)], EXPR$3=[MAX($5)], $g=[GROUPING($2, $3, $7)])
EnumerableTableScan(table=[[scott, EMP]])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the change expected?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the diff lays in grouping keys sequence ( [2,3,7] vs [3,7,2]). They should be equivalent.

return null;
}

RelOptCost lowerBound = null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If current group is not fully optimized and has no winner, there may be problems to get the lower bound here. Since the current group's optimization may affact the input groups of the current group. E.g., the current group's optimization may push down some require traits and maybe somewhere of the inputs subtrees may enhance there's costs. So if we get the lower bound here, it may be larger than the actual lower bound which may miss optimal plan

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You‘re right. If any logical node in the same RelSet is not built, looking for the lower bound of current RelSubset's member is not valid. Will remove this logic

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved


private final VolcanoPlanner planner;

private final Map<Integer, List<VolcanoRuleMatch>> matches = new HashMap<>();
Copy link
Member

@hsyuan hsyuan Jul 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use IdentityHashMap, the key can be RelNode.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved


public MetadataDef<LowerBoundCost> getDef() {
return BuiltInMetadata.LowerBoundCost.DEF;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently every time we add a new relnode through rule transformation, the whole metadata cache will be invalidated, which is not helpful for caching lower bound value.


public RelOptCost getLowerBoundCost(AbstractConverter ac,
RelMetadataQuery mq, RelOptPlanner planner) {
return mq.getLowerBoundCost(ac.getInput(), planner);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, once you add it, it will be hard to remove it and will always maintain it. Let's remove it.


/** Returns whether to skip a match. This happens if any of the
* {@link RelNode}s have importance zero. */
boolean skipMatch(VolcanoRuleMatch match) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not let RuleQueue be an Abstract class, and put these methods there?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Currently, the planner is in charge of matching rules and pruning nodes (by importances or other mechanism). So I think it is more natural that planner judges whether a rule match is validate or not.
  2. By moving to Planner, it could be easier to overwrite the logic by users, adding customized pruning logic

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved back

protected RelOptCost upperBoundForInputs(
RelNode mExpr, RelOptCost upperBound) {
if (!upperBound.isInfinite()) {
RelOptCost rootCost = mExpr.computeSelfCost(this,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

computeSelfCost is deprecated...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

if (!rel.getTraitSet().satisfies(group.getTraitSet())) {
RelNode passThroughRel = convert(rel, group);
if (passThroughRel == null) {
if (LOGGER.isDebugEnabled()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check if (LOGGER.isDebugEnabled()) { is redundant.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

RelOptCost upperBound = group.upperBound;
RelOptCost upperForInput = planner.upperBoundForInputs(mExpr, upperBound);
if (upperForInput.isLe(planner.zeroCost)) {
if (LOGGER.isDebugEnabled()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the check if (LOGGER.isDebugEnabled()) { in this class are redundant.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

* information regarding the contents of this Map and how it is initialized.
*/
private final Map<VolcanoPlannerPhase, Set<String>> phaseRuleMapping;
public abstract class RuleQueue {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove public

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

@hsyuan
Copy link
Member

hsyuan commented Jul 14, 2020

Yes, doing the final round review

jinpeng.wjp added 7 commits July 14, 2020 12:11
… pruning in calcite:

1. use RelNode as cache key
2. fix minor bugs found during tests
… pruning in calcite:

1. resolve some comments
2. fix ut
… pruning in calcite: resolve some comments
… pruning in calcite: resolve some comments
… pruning in calcite: some minor reconstructions
Copy link
Member

@hsyuan hsyuan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the excellent work, @FatLittle!

@hsyuan hsyuan added the LGTM-will-merge-soon Overall PR looks OK. Only minor things left. label Jul 14, 2020
pruning in calcite: add lower bound check for derive traits