-
Notifications
You must be signed in to change notification settings - Fork 119
Consider number of predicates at each level in rewriting cost model #3681
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
base: main
Are you sure you want to change the base?
Consider number of predicates at each level in rewriting cost model #3681
Conversation
3b65482 to
65694f8
Compare
65694f8 to
ca8d8c9
Compare
| task_count: 2476 | ||
| task_total_time_ms: 212 | ||
| transform_count: 467 | ||
| transform_time_ms: 67 | ||
| transform_yield_count: 143 | ||
| insert_time_ms: 21 | ||
| insert_new_count: 315 | ||
| insert_reused_count: 44 | ||
| task_count: 1325 | ||
| task_total_time_ms: 267 | ||
| transform_count: 270 | ||
| transform_time_ms: 153 | ||
| transform_yield_count: 83 | ||
| insert_time_ms: 12 | ||
| insert_new_count: 165 | ||
| insert_reused_count: 24 |
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 think the planning metrics for the two queries here have improved because the rewriting cost model now prefers expressions with less total number of predicates, so when the predicates in those two queries are simplified to remove the duplicated predicate, we end up with a smaller search space. Before the change here, the two expressions (the original expressions with the duplicated predicate and the simplified one) were considered equal and the semantic hashcode tie breaking resulted in the original expression being chosen.
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 looks almost suspicious that the time spent is higher even though all the counts are lower. Can you do a test for me? Remove all metrics files (metrics.binpb and metrics.yaml) try to run the entire suite in correction mode. Just glance over the time spent to see if there is a trend up or downwards. In general, this sort of stuff happens that we see confusing time durations as everyone uses a different set-up, had their Mac throttled or not, etc. -- so a sample size of one is pretty naive to comment on but still let's run this to just rule out the case that for some reason a rule has degraded. Also, you may want to do this for down stream.
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.
See #3681 (comment), I did multiple runs of correcting the entire test suite (including downstream) and there doesn't seem to be any consistent downtrend/uptrend in the time metrics.
ca8d8c9 to
cd4e175
Compare
normen662
left a comment
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.
Hi, good stuff! I left detailed (I hope) comments on your changes. In general I would say that the following should be focused on:
- take the new properties out of the expression property map, i.e. make them untracked
- investigate the interaction with predicate complexity (would it be better to reorder and maybe get rid of predicate height)
- evaluate if there is a planner performance regression
.../apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java
Show resolved
Hide resolved
| * deeper level or if {@code a} has a higher number of levels with predicates. | ||
| */ | ||
| public static int compare(final PredicateCountByLevelInfo a, final PredicateCountByLevelInfo b) { | ||
| final int highestLevel = Integer.max(a.getHighestLevel(), b.getHighestLevel()); |
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.
Wouldn't it be more concise to use two SortedMaps (TreeMap). You can then pop them off smallest first on both sides. In that way you can get rid of the highestLevel field which is redundant in your data structure.
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.
Thanks for the suggestion, that is indeed better! I modified the code to do that and removed the highestLevel field in the data structure. There is a way to do this with the Java Streams API but I found it to be less readable than a simple for-loop, let me know if you think otherwise or if there is a better way to implement this.
I did have to keep the getHighestLevel method on the data structure just because the ImmutableSortedMap throws an exception for empty maps if lastKey is called on them so having the method is a neat way to wrap that check.
.../apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java
Outdated
Show resolved
Hide resolved
.../apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java
Show resolved
Hide resolved
.../apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java
Outdated
Show resolved
Hide resolved
| ImmutableSet.of(), | ||
| ImmutableSet.of(ExpressionCountProperty.selectCount(), ExpressionCountProperty.tableFunctionCount(), | ||
| PredicateComplexityProperty.predicateComplexity()), | ||
| ImmutableSet.of( |
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.
Ah, I think this should really selectCount and tableFunctionCount in there. I think predicateComplexity should not be in here but 🤷 . I think the new ones you are introducing should not be 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.
Just a note that I had to leave predicateComplexity here because it is used in the SelectMergeRule in a code path that expects it to be tracked (
Lines 114 to 118 in aaf3961
| public static <E extends RelationalExpression> BiFunction<ExpressionPartition<E>, ? super E, Tuple> comparisonByPropertyList(@Nonnull ExpressionProperty<?>... expressionProperties) { | |
| return (partition, expression) -> | |
| Tuple.fromItems(Arrays.stream(expressionProperties) | |
| .map(property -> partition.getNonPartitioningPropertyValue(expression, property)) | |
| .collect(Collectors.toList())); |
| int bPredicateHeight = predicateHeight().evaluate(b); | ||
| if (aPredicateHeight != bPredicateHeight) { | ||
| return Integer.compare(aPredicateHeight, bPredicateHeight); | ||
| int aPredicateCount = predicateCount().evaluate(a); |
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.
How does all of this (not picking out this particular line but this entire added block here) interact with the predicate complexity? Wouldn't predicate complexity be subsuming the predicate height property you are introducing? Could we put the predicate complexity in front of the count-by-layer property 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.
As discussed offline, I changed this to use the NormalizedResidualPredicateProperty instead as the first thing to check, which works nicely because it takes care of both the comparison of predicate count (but in a better way because it naturally prefers simpler predicates), and it also makes it unnecessary to consider the PredicateComplexityProperty.
The reason why we don't need to consider the PredicateComplexityProperty anymore is that if there is an expression that has a query predicate that has a worse predicate complexity (i.e. large tree diameter), it would contribute in the same way to the number of conjuncts in the normalized form of the combined query predicate. Otherwise this would mean that a predicate with a higher tree diameter (i.e. lots of nested predicates) would have a smaller number of conjuncts in the normal form than another predicate that has a smaller tree diameter, which doesn't make sense.
I also confirmed downstream that removing the check for PredicateComplexityProperty doesn't change anything.
| @Test | ||
| void compareReturnsInfoWithMoreLevelsInCaseOfEquality() { | ||
| final PredicateCountByLevelProperty.PredicateCountByLevelInfo aInfo = new PredicateCountByLevelProperty.PredicateCountByLevelInfo( | ||
| Map.of(1, 1, 2, 3, 3, 1), 3); |
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.
Another weird idiosyncrasy: All maps in the record layer have to be ImmutableMap (guava) instead of java.util.Map. The reason is that their copy-constructors avoid a copy if the source is a ImmutableMap. Map does that, too, but only if the source is a Map as well. So while it would be better to have the entire codebase on Map and not on ImmutableMap, someone would have to change everything first. The same applies to lists and sets as well.
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 replaced all usages with that, thanks for the clarification!
.../apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java
Outdated
Show resolved
Hide resolved
| task_count: 2476 | ||
| task_total_time_ms: 212 | ||
| transform_count: 467 | ||
| transform_time_ms: 67 | ||
| transform_yield_count: 143 | ||
| insert_time_ms: 21 | ||
| insert_new_count: 315 | ||
| insert_reused_count: 44 | ||
| task_count: 1325 | ||
| task_total_time_ms: 267 | ||
| transform_count: 270 | ||
| transform_time_ms: 153 | ||
| transform_yield_count: 83 | ||
| insert_time_ms: 12 | ||
| insert_new_count: 165 | ||
| insert_reused_count: 24 |
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 looks almost suspicious that the time spent is higher even though all the counts are lower. Can you do a test for me? Remove all metrics files (metrics.binpb and metrics.yaml) try to run the entire suite in correction mode. Just glance over the time spent to see if there is a trend up or downwards. In general, this sort of stuff happens that we see confusing time durations as everyone uses a different set-up, had their Mac throttled or not, etc. -- so a sample size of one is pretty naive to comment on but still let's run this to just rule out the case that for some reason a rule has degraded. Also, you may want to do this for down stream.
Using this property is better for multiple reasons: 1) It would naturally prefer simpler query predicates over complex ones, as the simpler predicates would result in a simpler combined normalized query predicate for the entire QGM. 2) It takes into consideration predicates that were simplified to be a tautology, making sure these predicates are preferred over unsimplified ones. A simpler predicate count is not able to do this. By using the NormalizedResidualPredicateProperty, it is no longer necessary to use the PredicateComplexityProperty in the RewritingCostModel as the NormalizedResidualPredicateProperty would take care of choosing the expreission that has the least maximal query predicate across its QGM (as that would lead to fewer conjuncts in the normalized form of the combined query predicate).
After testing against existing yaml-tests, this leads to slightly worse performance with no gain, due to having to calculate this property for many expressions that will end up being pruned by other properties considered in the rewriting cost model earlier.
For consistency with the rest of the codebase.
📊 Metrics Diff Analysis ReportSummary
ℹ️ About this analysisThis automated analysis compares query planner metrics between the base branch and this PR. It categorizes changes into:
The last category in particular may indicate planner regressions that should be investigated. New QueriesCount of new queries by file:
Plan and Metrics ChangedThese queries experienced both plan and metrics changes. This generally indicates that there was some planner change Total: 2 queries Statistical Summary (Plan and Metrics Changed)
Significant Regressions (Plan and Metrics Changed)There were 2 outliers detected. Outlier queries have a significant regression in at least one field. Statistically, this represents either an increase of more than two standard deviations above the mean or a large absolute increase (e.g., 100).
Only Metrics ChangedThese queries experienced only metrics changes without any plan changes. If these metrics have substantially changed, Total: 2 queries Statistical Summary (Only Metrics Changed)
Significant Regressions (Only Metrics Changed)There were 2 outliers detected. Outlier queries have a significant regression in at least one field. Statistically, this represents either an increase of more than two standard deviations above the mean or a large absolute increase (e.g., 100).
|
This PR improves the rewriting cost model by making it consider the number of predicates in each expression at each level of the expression graph, instead of comparing graphs based on the height of highest expressions in the graph that contains any predicates.
The reasoning for this is to push the cost model to prefer expressions with the same number of predicates, where some predicates were pushed from higher levels in the QGM to lower levels, which can lead to producing plans with more specific index key comparisons in the planning phase.
Example: the rewriting cost model should consider the expression:
to have a lower cost than:
The rewriting cost model is improved by considering two
ExpressionPropertywhen comparing two expressions: the existingNormalizedResidualPredicatePropertyproperty which calculates the total number of conjuncts in the combined query predicate across the entire expression tree at all levels and a new propertyPredicateCountByLevelPropertywhich calculates the number of predicates at each level of the expression graph. In addition, considering the propertyPredicateComplexityPropertyis no longer necessary as what it checks for (the most complex predicate across the entire expression tree) is already checked indirectly byNormalizedResidualPredicateProperty.Performance impact
Some basic analysis of the performance impact of considering those properties can be found in #3681 (comment).