-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Conversation
@@ -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); |
There was a problem hiding this comment.
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
core/src/test/resources/sql/misc.iq
Outdated
@@ -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]) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
core/src/main/java/org/apache/calcite/config/CalciteConnectionProperty.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
Outdated
Show resolved
Hide resolved
clearProcessed(planner.getSet(parentRel)); | ||
} | ||
if (subset == planner.root) { | ||
tasks.push(new OptimizeGroup(subset, planner.infCost)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
ef9dae4
to
f21f99d
Compare
|
||
public MetadataDef<LowerBoundCost> getDef() { | ||
return BuiltInMetadata.LowerBoundCost.DEF; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
- Different subclass of RelNodes may have different logic, like AC, RelNode, SpoolNode, RelSubset ...
- It needs a cache and the cache should be invalidated when its input is changed.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- 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.
- 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.
- 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still use abstract converter?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
4c2f0f3
to
62e2f79
Compare
} catch (VolcanoTimeoutException e) { | ||
planner.canonize(); | ||
ruleQueue.phaseCompleted(phase); | ||
break PLANNING; |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
)
ee751f5
to
308f360
Compare
/** | ||
* RelNode ids that is invoked passThrough method before | ||
*/ | ||
Set<RelNode> passThroughCache; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct the comment too~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
core/src/test/resources/sql/agg.iq
Outdated
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]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the change expected?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
2d79067
to
9127f69
Compare
|
||
private final VolcanoPlanner planner; | ||
|
||
private final Map<Integer, List<VolcanoRuleMatch>> matches = new HashMap<>(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; | ||
} |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 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.
- By moving to Planner, it could be easier to overwrite the logic by users, adding customized pruning logic
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
computeSelfCost is deprecated...
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove public
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
Yes, doing the final round review |
… pruning in calcite
pruning in calcite: resolve some comments
… 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
bef8b7b
to
9001db1
Compare
There was a problem hiding this 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!
pruning in calcite: add lower bound check for derive traits
|
||
/** | ||
* gets the rule queue | ||
*/ |
related to #1950