Skip to content

[CALCITE-5240] Enhance MaterializedViewRule so that it applies to rol…#2876

Open
twdsilva wants to merge 3 commits into
apache:mainfrom
twdsilva:CALCITE-5240
Open

[CALCITE-5240] Enhance MaterializedViewRule so that it applies to rol…#2876
twdsilva wants to merge 3 commits into
apache:mainfrom
twdsilva:CALCITE-5240

Conversation

@twdsilva

Copy link
Copy Markdown

…lup view for queries that contain a predicate on the rollup column

…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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@twdsilva twdsilva Sep 23, 2022

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated the PR with this change, thanks for the review @sdreynolds.

@twdsilva
twdsilva force-pushed the CALCITE-5240 branch 2 times, most recently from c761219 to 509149c Compare September 23, 2022 20:01
Comment on lines +305 to +327
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's worth extracting the RelShuttleImpl to a private class to improve readability

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

@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()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

queryPreds.right is nullable here, we need to check for queryPreds.right != null before invoking a method to avoid NPE

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Comment on lines +970 to +974
ImplicitViewPredicateShuttle viewPredicateShuttle2 =
new ImplicitViewPredicateShuttle(rexBuilder, rexCall, rexInputRef, true);
RexNode viewPredicate = queryPreds.right.accept(viewPredicateShuttle);
if (viewPredicateShuttle.isRangeMatched()) {
RexNode viewFilter = queryPreds.right.accept(viewPredicateShuttle2);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can move the init of the second shuttle inside the if, the only place where it's used.

Suggested change
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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

int viewColumnIndex = 0;
for (RexNode rexNode : project.getProjects()) {
if (rexNode instanceof RexCall) {
RexCall rexCall = (RexCall) rexNode;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a method called getViewAndViewPredicate.

Comment on lines +1302 to +1303
RexInputRef rexInputRef,
boolean generateViewFilter) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: we generally don't stack up arguments in Calcite

Suggested change
RexInputRef rexInputRef,
boolean generateViewFilter) {
RexInputRef rexInputRef, boolean generateViewFilter) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

removed the generateViewFilter param as its no longer required

Comment on lines +1345 to +1348
RexNode modifiedCall = rexBuilder.makeCall(call.getType(), transformedCallOperator,
reverseOperands ? ImmutableList.of(tableInputRefOperand, truncatedLiteral)
: ImmutableList.of(truncatedLiteral, tableInputRefOperand));
return modifiedCall;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

this.generateViewFilter = generateViewFilter;
}

private RexNode transformCall(RexCall call, boolean isLowerBound) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need to split this method using auxiliary ones, can you rework it to reduce the overall complexity?

@twdsilva twdsilva Dec 3, 2022

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added a method adjustComparisonBoundary .

Comment on lines +1317 to +1323
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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about simplifying the if condition by introducing some boolean variables like isLeftLiteral, isRightLiteral etc?

Suggested change
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) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

reverseOperands = true;
}
int predicateIndex = ((RexTableInputRef) tableInputRefOperand).getIndex();
if (floorIndex == predicateIndex) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here we could probably replace the if/else with a call to an aux method

@twdsilva twdsilva Dec 3, 2022

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I added the new method adjustComparisonBoundary to handle the if case here.

Comment on lines +1364 to +1370
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);
}

@asolimando asolimando Nov 13, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Comment on lines +1373 to +1375
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add an example here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

}

public boolean isRangeMatched() {
return lowerBound != 0L || upperBound != 0L && lowerBound != upperBound;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't you mean the following?

Suggested change
return lowerBound != 0L || upperBound != 0L && lowerBound != upperBound;
return (lowerBound != 0L || upperBound != 0L) && lowerBound != upperBound;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

.ok();
}

@Test void testAggregateMaterializationAggregateFuncsRange1() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

this.generateViewFilter = generateViewFilter;
}

private RexNode transformCall(RexCall call, boolean isLowerBound) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I rename transformCall into adjustComparisonBoundary, transformComparisonBoundary or something closer to what this method is actually doing

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about dropping res and returning the expressions?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Comment on lines 359 to +357
default:
return TimeUnitRange.DAY;
return SqlFunctions.ceil(v, unit.startUnit.multiplier.longValue());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the default case is just for ceil, I'd rather cover it explicitly

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

is(false));
}

private void helpTestFloorCeil(String timestampString, String expectedFloorString,

@asolimando asolimando Nov 13, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

assertFloorCeil or checkFloorCeil?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

private void helpTestFloorCeil(String timestampString, String expectedFloorString,
String expectedCeilString, TimeUnitRange timeUnitRange) {
final RexLiteral timestampLiteral = rexBuilder.makeTimestampLiteral(
new TimestampString(timestampString), 3);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is precision 3 always appropriate?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes because the input timestamp string is always contains milliseconds.

@asolimando asolimando left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added final modifier here

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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

}

@Test void testAggregateMaterializationAggregateFuncsRange31() {
// test using multiple views

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a more descriptive comment.

.ok();
}


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: remove the extra newline here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Unnecessary boxing, it can be simplified as:

Suggested change
selectivity += (upperBound - lowerBound) / (Double.valueOf(DateTimeUtils.MILLIS_PER_DAY));
selectivity += (upperBound - lowerBound) / ((double) DateTimeUtils.MILLIS_PER_DAY);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

.checkingThatResultContains("EnumerableTableScan(table=[[hr, events]])")
.ok();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

@twdsilva
twdsilva force-pushed the CALCITE-5240 branch 2 times, most recently from c51b075 to ce29d31 Compare December 3, 2022 02:40
@twdsilva

twdsilva commented Dec 3, 2022

Copy link
Copy Markdown
Author

@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.

Thank you @asolimando for the review.
We have a use case where we maintain materialized preaggregated cubes that store data rolled up to the UTC day or hour. The queries in our system always have a time range predicate that doesn't neccesarily align on the UTC day or boundary. We would like to use the materialized cube(s) table whenever possible as they contain a lot fewer rows than the base table. Here is an example.
I could not figure how to get MaterializedViewAggregateRule to handle the case when a view contains a rollup (has a group by with a FLOOR(col)) other than rewriting the view to include a predicate based on the query predicate. The existing code that uses the union operator works if we include this generated predicate. The other main change is to map FLOOR(col) to col which is valid because the predicate added to the view is aligned on the floored value of the query predicate. If there is a cleaner way to make this work I would appreciate any pointers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants