[CALCITE-5240] Enhance MaterializedViewRule so that it applies to rol…#2876
[CALCITE-5240] Enhance MaterializedViewRule so that it applies to rol…#2876twdsilva wants to merge 3 commits into
Conversation
…lup view for queries that contain a predicate on the rollup column
| for (RexNode rexNode : project.getProjects()) { | ||
| if (rexNode instanceof RexCall) { | ||
| RexCall rexCall = (RexCall) rexNode; | ||
| if (rexCall.getOperator() instanceof SqlFloorFunction) { |
There was a problem hiding this comment.
Should this compare based on SqlKind ? https://github.com/apache/calcite/blob/b9c2099ea92a575084b55a206efc5dd341c0df62/core/src/main/java/org/apache/calcite/sql/SqlKind.java
There was a problem hiding this comment.
Updated the PR with this change, thanks for the review @sdreynolds.
c761219 to
509149c
Compare
509149c to
381459c
Compare
| new RelShuttleImpl() { | ||
| @Override public RelNode visit(LogicalFilter filter) { | ||
| RexNode condition = | ||
| RexUtil.flatten(rexBuilder, | ||
| rexBuilder.makeCall( | ||
| SqlStdOperatorTable.AND, | ||
| filter.getCondition(), | ||
| queryCompensationPred)); | ||
| return filter.copy(filter.getTraitSet(), filter.getInput(), condition); | ||
| } | ||
|
|
||
| @Override public RelNode visit(RelNode other) { | ||
| if (other instanceof RelSubset) { | ||
| RelSubset relSubset = (RelSubset) other; | ||
| return relSubset.getBestOrOriginal().accept(this); | ||
| } else { | ||
| return visitChildren(other); | ||
| } | ||
| } | ||
| }); | ||
| } else { | ||
| rewrittenPlan = relBuilder | ||
| .push(target) |
There was a problem hiding this comment.
I think it's worth extracting the RelShuttleImpl to a private class to improve readability
| @Override public @Nullable Pair<RelNode, RexNode> rewriteInputView(RelOptRuleCall call, | ||
| RelNode view, RelNode viewNode, RexBuilder rexBuilder, Pair<RexNode, RexNode> queryPreds, | ||
| RexNode viewPred) { | ||
| if (!queryPreds.right.isAlwaysTrue() && viewPred.isAlwaysTrue()) { |
There was a problem hiding this comment.
queryPreds.right is nullable here, we need to check for queryPreds.right != null before invoking a method to avoid NPE
| ImplicitViewPredicateShuttle viewPredicateShuttle2 = | ||
| new ImplicitViewPredicateShuttle(rexBuilder, rexCall, rexInputRef, true); | ||
| RexNode viewPredicate = queryPreds.right.accept(viewPredicateShuttle); | ||
| if (viewPredicateShuttle.isRangeMatched()) { | ||
| RexNode viewFilter = queryPreds.right.accept(viewPredicateShuttle2); |
There was a problem hiding this comment.
We can move the init of the second shuttle inside the if, the only place where it's used.
| ImplicitViewPredicateShuttle viewPredicateShuttle2 = | |
| new ImplicitViewPredicateShuttle(rexBuilder, rexCall, rexInputRef, true); | |
| RexNode viewPredicate = queryPreds.right.accept(viewPredicateShuttle); | |
| if (viewPredicateShuttle.isRangeMatched()) { | |
| RexNode viewFilter = queryPreds.right.accept(viewPredicateShuttle2); | |
| RexNode viewPredicate = queryPreds.right.accept(viewPredicateShuttle); | |
| if (viewPredicateShuttle.isRangeMatched()) { | |
| ImplicitViewPredicateShuttle viewPredicateShuttle2 = | |
| new ImplicitViewPredicateShuttle(rexBuilder, rexCall, rexInputRef, true); | |
| RexNode viewFilter = queryPreds.right.accept(viewPredicateShuttle2); |
Additionally, let's find a more descriptive name than viewPredicateShuttle, something likeviewPredicateShuttleFilterGenerator?
Possibly let's improve also viewPredicateShuttle, with something like viewPredicateShuttleRangeMatcher?
| int viewColumnIndex = 0; | ||
| for (RexNode rexNode : project.getProjects()) { | ||
| if (rexNode instanceof RexCall) { | ||
| RexCall rexCall = (RexCall) rexNode; |
There was a problem hiding this comment.
I feel this is the right spot where an auxiliary method could be extracted, rewriteInputView is too long and does a lot of things, what do you think?
There was a problem hiding this comment.
Added a method called getViewAndViewPredicate.
| RexInputRef rexInputRef, | ||
| boolean generateViewFilter) { |
There was a problem hiding this comment.
Nit: we generally don't stack up arguments in Calcite
| RexInputRef rexInputRef, | |
| boolean generateViewFilter) { | |
| RexInputRef rexInputRef, boolean generateViewFilter) { |
There was a problem hiding this comment.
removed the generateViewFilter param as its no longer required
| RexNode modifiedCall = rexBuilder.makeCall(call.getType(), transformedCallOperator, | ||
| reverseOperands ? ImmutableList.of(tableInputRefOperand, truncatedLiteral) | ||
| : ImmutableList.of(truncatedLiteral, tableInputRefOperand)); | ||
| return modifiedCall; |
There was a problem hiding this comment.
I guess you use the variable for easing debug, for modern debuggers can step past the return expression, I think we lose clarity and we don't gain much, I suggest to drop the variable and return immediately.
| this.generateViewFilter = generateViewFilter; | ||
| } | ||
|
|
||
| private RexNode transformCall(RexCall call, boolean isLowerBound) { |
There was a problem hiding this comment.
We need to split this method using auxiliary ones, can you rework it to reduce the overall complexity?
There was a problem hiding this comment.
added a method adjustComparisonBoundary .
| boolean reverseOperands = false; | ||
| if ((literalOperand.getKind() == SqlKind.LITERAL | ||
| && tableInputRefOperand.getKind() == SqlKind.TABLE_INPUT_REF) | ||
| || (literalOperand.getKind() == SqlKind.TABLE_INPUT_REF | ||
| && tableInputRefOperand.getKind() == SqlKind.LITERAL)) { | ||
| if (literalOperand.getKind() == SqlKind.TABLE_INPUT_REF | ||
| && tableInputRefOperand.getKind() == SqlKind.LITERAL) { |
There was a problem hiding this comment.
What about simplifying the if condition by introducing some boolean variables like isLeftLiteral, isRightLiteral etc?
| boolean reverseOperands = false; | |
| if ((literalOperand.getKind() == SqlKind.LITERAL | |
| && tableInputRefOperand.getKind() == SqlKind.TABLE_INPUT_REF) | |
| || (literalOperand.getKind() == SqlKind.TABLE_INPUT_REF | |
| && tableInputRefOperand.getKind() == SqlKind.LITERAL)) { | |
| if (literalOperand.getKind() == SqlKind.TABLE_INPUT_REF | |
| && tableInputRefOperand.getKind() == SqlKind.LITERAL) { | |
| final boolean isLeftLiteral = literalOperand.getKind() == SqlKind.LITERAL; | |
| final boolean isRightLiteral = tableInputRefOperand.getKind() == SqlKind.LITERAL; | |
| final boolean isLeftTableInputRef = literalOperand.getKind() == SqlKind.TABLE_INPUT_REF; | |
| final boolean isRightTableInputRef = tableInputRefOperand.getKind() == SqlKind.TABLE_INPUT_REF; | |
| boolean reverseOperands = false; | |
| if (isLeftLiteral && isRightTableInputRef || isLeftTableInputRef && isRightLiteral) { | |
| if (literalOperand.getKind() == SqlKind.TABLE_INPUT_REF | |
| && tableInputRefOperand.getKind() == SqlKind.LITERAL) { |
| reverseOperands = true; | ||
| } | ||
| int predicateIndex = ((RexTableInputRef) tableInputRefOperand).getIndex(); | ||
| if (floorIndex == predicateIndex) { |
There was a problem hiding this comment.
Here we could probably replace the if/else with a call to an aux method
There was a problem hiding this comment.
I added the new method adjustComparisonBoundary to handle the if case here.
| if (v0 == null) { | ||
| throw new AssertionError("interpreter returned null for " + v0); | ||
| } | ||
| Comparable v1 = RexInterpreter.evaluate(literalOperand, Collections.emptyMap()); | ||
| if (v1 == null) { | ||
| throw new AssertionError("interpreter returned null for " + v1); | ||
| } |
There was a problem hiding this comment.
v0 and v1 are always null when you build the assertion message, replace v0/v1 with truncatedLiteral and literalOperand (i.e., what you were evaluating when null was returned)
Moreover, sentences should start with a capital letter
| // Since the view contains a FLOOR() if the query does a > or <= and the operand is already | ||
| // a FLOORED value we have to shift the value to the next higher or lower floored value | ||
| // If the query contains a >= or < we don't need to shift the modified value |
There was a problem hiding this comment.
Can you add an example here?
| } | ||
|
|
||
| public boolean isRangeMatched() { | ||
| return lowerBound != 0L || upperBound != 0L && lowerBound != upperBound; |
There was a problem hiding this comment.
Don't you mean the following?
| return lowerBound != 0L || upperBound != 0L && lowerBound != upperBound; | |
| return (lowerBound != 0L || upperBound != 0L) && lowerBound != upperBound; |
| .ok(); | ||
| } | ||
|
|
||
| @Test void testAggregateMaterializationAggregateFuncsRange1() { |
There was a problem hiding this comment.
Please find better names for tests, if you have multiple tests, they are covering something specific each, so why don't put that in the test name? This is true for all tests1...n in the PR
There was a problem hiding this comment.
changed test names to be more descriptive
| // ignore predicates where the type isn't a timestamp | ||
| return rexBuilder.makeLiteral(true); | ||
| } | ||
| RexNode lb = rexBuilder.makeTimestampLiteral((TimestampString) r.lowerEndpoint(), 0); |
There was a problem hiding this comment.
Please avoid cryptic names like lb, ub, there is already a lot of complex logic in these methods, let's try to help the code reader with more explicit names like lowerBound and upperBound.
They are surely longer, but nowadays we have IDEs for that :)
| this.generateViewFilter = generateViewFilter; | ||
| } | ||
|
|
||
| private RexNode transformCall(RexCall call, boolean isLowerBound) { |
There was a problem hiding this comment.
I rename transformCall into adjustComparisonBoundary, transformComparisonBoundary or something closer to what this method is actually doing
| Sarg modSarg = Sarg.of(RexUnknownAs.UNKNOWN, | ||
| ImmutableRangeSet.of(Range.closedOpen(lbTimestampString, ubTimestampString))); | ||
| RexNode searchArgumentLiteral = rexBuilder.makeSearchArgumentLiteral(modSarg, type); | ||
| RexNode res = rexBuilder.makeCall(SqlStdOperatorTable.SEARCH, tableInputRefOperand, |
There was a problem hiding this comment.
What about dropping res and returning the expressions?
| default: | ||
| return TimeUnitRange.DAY; | ||
| return SqlFunctions.ceil(v, unit.startUnit.multiplier.longValue()); |
There was a problem hiding this comment.
If the default case is just for ceil, I'd rather cover it explicitly
| is(false)); | ||
| } | ||
|
|
||
| private void helpTestFloorCeil(String timestampString, String expectedFloorString, |
There was a problem hiding this comment.
assertFloorCeil or checkFloorCeil?
| private void helpTestFloorCeil(String timestampString, String expectedFloorString, | ||
| String expectedCeilString, TimeUnitRange timeUnitRange) { | ||
| final RexLiteral timestampLiteral = rexBuilder.makeTimestampLiteral( | ||
| new TimestampString(timestampString), 3); |
There was a problem hiding this comment.
Is precision 3 always appropriate?
There was a problem hiding this comment.
Yes because the input timestamp string is always contains milliseconds.
asolimando
left a comment
There was a problem hiding this comment.
@twdsilva thanks for the PR, I think it's a very useful feature, I still share the doubts that @julianhyde raised in a jira comment (copying a filter into the view seems hacky and does not always seem safe) and that @zabetak raised in the ML (isn't there a way to modify the query to match the view, instead of changing the view and making view matching more complex?).
In the same ML discussion, you have raised a question on this point, I don't have an answer to that myself, but I feel that the we all need to agree that we are going into the right direction before being able to merge the PR.
| long expectedFloorValue = new TimestampString(expectedFloorString).getMillisSinceEpoch(); | ||
| assertThat(eval(flooredLiteral), is(expectedFloorValue)); | ||
|
|
||
| RexNode ceiledLiteral = rexBuilder.makeCall(SqlStdOperatorTable.CEIL, |
There was a problem hiding this comment.
Please be consistent with the final modifier, since you have added it on some other vars, if you omit it here, the reader thinks it's going to mutate, while this is not the case
| TimeUnitRange.HOUR); | ||
| helpTestFloorCeil(timestampString, "2011-07-20 00:00:00", "2011-07-21 00:00:00", | ||
| TimeUnitRange.DAY); | ||
| helpTestFloorCeil("2011-07-20 12:34:59.078", "2011-07-17 00:00:00", "2011-07-24 00:00:00", |
There was a problem hiding this comment.
The start of week here is on Sunday, which is locale dependant, is there a way to make this locale dependant? If not, why would it be OK as is?
There was a problem hiding this comment.
From the comment on CALCITE-3412 I think FLOOR(timestamp TO WEEK) will always round down to the previous Sunday.
https://issues.apache.org/jira/browse/CALCITE-3412?focusedCommentId=16953504&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-16953504
| } | ||
|
|
||
| @Test void testAggregateMaterializationAggregateFuncsRange31() { | ||
| // test using multiple views |
There was a problem hiding this comment.
can you improve the comment to match the explanation of the tests above which is of great help? Notably, please cover why mv0 is picked over mv1 and sketch what happens when multiple views are available
There was a problem hiding this comment.
Added a more descriptive comment.
| .ok(); | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
Nit: remove the extra newline here
| long lowerBound = ((TimestampString) r.lowerEndpoint()).getMillisSinceEpoch(); | ||
| long upperBound = ((TimestampString) r.upperEndpoint()).getMillisSinceEpoch(); | ||
| // only used for a range less than one day | ||
| selectivity += (upperBound - lowerBound) / (Double.valueOf(DateTimeUtils.MILLIS_PER_DAY)); |
There was a problem hiding this comment.
Unnecessary boxing, it can be simplified as:
| selectivity += (upperBound - lowerBound) / (Double.valueOf(DateTimeUtils.MILLIS_PER_DAY)); | |
| selectivity += (upperBound - lowerBound) / ((double) DateTimeUtils.MILLIS_PER_DAY); |
| .checkingThatResultContains("EnumerableTableScan(table=[[hr, events]])") | ||
| .ok(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Could you add one or more tests explicitly covering your claim that the behaviour is unchanged when we have more complex cases (a predicate already exists, we have a predicate in a join, etc.)?
There was a problem hiding this comment.
I have added testAggregateViewWithPredicate to test when a predicate already exists in the view and testJoinViewWithoutPredicate, testJoinViewWithPredicateSameAsQueryPredicate, testJoinViewWithPredicateDifferentThanQueryPredicate to test views that have a join with predicate.
c51b075 to
ce29d31
Compare
ce29d31 to
a6007c7
Compare
Thank you @asolimando for the review. |
8a5cf83 to
cf7f71b
Compare
…lup view for queries that contain a predicate on the rollup column