-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Compute all groups for group by queries with only filtered aggregations #14211
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
Compute all groups for group by queries with only filtered aggregations #14211
Conversation
The PR description explains the rationale behind choosing different default behavior for the v1 and v2 query engines here. However, this currently means that there's no way to disable this behavior for the multi-stage query engine to improve query performance (and get non-standard behavior as a tradeoff). If we want to instead keep the behavior of the two query engines consistent, we have two options:
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #14211 +/- ##
============================================
+ Coverage 61.75% 63.81% +2.05%
- Complexity 207 1536 +1329
============================================
Files 2436 2623 +187
Lines 133233 144424 +11191
Branches 20636 22099 +1463
============================================
+ Hits 82274 92157 +9883
- Misses 44911 45462 +551
- Partials 6048 6805 +757
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
if (intKeys == null) { | ||
// _groupIdGenerator should still have all the groups even if there are only filtered aggregates for SQL | ||
// compliant results. | ||
generateGroupByKeys(block); | ||
} |
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 be semantically the same to always initialize intKeys
to generateGroupByKeys(block);
in line 209?
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.
Yup, this is actually left over from my earlier choice of making it optional for the multi-stage engine too. I can move it back if we decide we're okay with keeping this the default behavior for the multi-stage engine.
// Write the query in a way where the aggregation will not be pushed to the leaf stage, so that we can test the | ||
// MultistageGroupByExecutor |
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: I think you can use the is_skip_leaf_stage_group_by hint to do so
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.
Oh nice, I wasn't aware of the existence of this hint.
@@ -3729,4 +3729,39 @@ public void testSkipIndexes(boolean useMultiStageQueryEngine) | |||
updateTableConfig(tableConfig); | |||
reloadAllSegments(TEST_UPDATED_RANGE_INDEX_QUERY, true, numTotalDocs); | |||
} | |||
|
|||
@Test | |||
public void testFilteredAggregationWithNoValueMatchingAggregationFilter() |
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: can you create one test having the query option and another without 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.
Personally I think the current solution is fine, but in a future PR it would be cool to support filteredAggregationsComputeAllGroups
query option in multi-stage 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.
but in a future PR it would be cool to support filteredAggregationsComputeAllGroups query option in multi-stage as well.
Do you mean keep this new default SQL compliant behavior but use the same query option to allow users to choose the other behavior (i.e., via SET filteredAggregationsComputeAllGroups=false
)? That should be straightforward and I can do it in this PR too if we have consensus. I was just a little worried about introducing new behavioral differences between the two query engines and any additional confusion about using a query option to get the opposite behavior in the engines. But I guess we might be fine with the right documentation.
if (intKeys == null) { | ||
// _groupIdGenerator should still have all the groups even if there are only filtered aggregates for SQL | ||
// compliant results. | ||
generateGroupByKeys(block); | ||
} |
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.
Yup, this is actually left over from my earlier choice of making it optional for the multi-stage engine too. I can move it back if we decide we're okay with keeping this the default behavior for the multi-stage engine.
// Write the query in a way where the aggregation will not be pushed to the leaf stage, so that we can test the | ||
// MultistageGroupByExecutor |
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.
Oh nice, I wasn't aware of the existence of this hint.
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.
Wow, this is much easier than expected!
@@ -384,7 +385,12 @@ public static List<AggregationInfo> buildFilteredAggregationInfos(SegmentContext | |||
} | |||
} | |||
|
|||
if (!nonFilteredFunctions.isEmpty()) { | |||
if (!nonFilteredFunctions.isEmpty() || QueryOptionsUtils.isFilteredAggregationsComputeAllGroups( |
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.
Wow, didn't expect this to be so simple! NIce!
@@ -241,6 +241,11 @@ private void processAggregate(TransferableBlock block) { | |||
aggFunction.aggregateGroupBySV(numMatchedRows, filteredIntKeys, groupByResultHolder, blockValSetMap); | |||
} | |||
} | |||
if (intKeys == 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.
Let's make this also controlled by query option to be consistent with v1
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.
Yep, makes sense, that's also what Gonzalo suggested.
// are expected to be returned). If this option is set to true, the v1 query engine will compute all groups for | ||
// group by queries with only filtered aggregations. This could require a full scan if the main query does not | ||
// have a filter and performance could be much worse, but the result will be SQL compliant. | ||
public static final String FILTERED_AGGREGATIONS_COMPUTE_ALL_GROUPS = "filteredAggregationsComputeAllGroups"; |
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.
Since the overhead seems fine (no extra aggregate needed), I'd suggest doing the standard SQL behavior by default, and add an option to skip the all groups computation
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.
Since the overhead seems fine (no extra aggregate needed), I'd suggest doing the standard SQL behavior by default, and add an option to skip the all groups computation
I think it makes sense to have standard SQL behavior by default and have an option to use the more performant method of only computing required groups. But won't the overhead potentially be quite high for a query like SELECT SUM(X) FILTER (WHERE Y = 1) FROM mytable
and there's an inverted index on the filter column Y
? If we want to compute all groups, it'll involve a scan over all docs whereas with the previous default behavior, no scan would be required and we'd only need to do the aggregation over the docs matching the filter computed using the inverted index. There won't be any additional aggregation being done but the scan would still be expensive right?
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.
That is true if the selectivity of the filter is very high. I'm okay with the current approach to ensure no performance regression. We can have a separate single option in the future to turn everything into standard SQL
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, I think we can have a startup config option to choose and the query option to change the default. It is true that having different behaviors by default on each query engine is strange and by having a startup config option each deployment can customize what do they want (current performance or sql compliance)
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've pushed a change to allow overriding the multi-stage engine behavior in order to prefer performance over SQL compliant results using the same query option filteredAggregationsComputeAllGroups
.
Yes, I think we can have a startup config option to choose and the query option to change the default.
I think we can add such a config as a follow up if there's demand for such behavior?
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.
Actually, thinking about this some more, I really don't like introducing additional behavioral differences between the two query engines. Also, using the same query option but with effectively opposite default values in the query engines is fairly unintuitive. I think it makes more sense to do the right thing by default, and allow users to override the behavior for performance reasons using a query option (which will have the same behavior in both query engines).
… in order to prefer performance over SQL compliant results using same query option
fc0edd3
to
68124da
Compare
… allow overriding behavior for performance
68124da
to
eb934a6
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! Please add the new query option to the documentation
This patch takes a different approach for both the query engines - in the single-stage engine, the default behavior remains the same, but the standard SQL behavior of returning all groups can be enabled using a new query option. In the multi-stage engine, however, all groups will be computed by default. The reasoning is that Pinot has historically chosen performance over standard SQL behavior for the v1 engine in such cases where the behavioral difference might not be too significant. For the multi-stage query engine, however, we have been moving towards standard SQL behavior regardless of potential performance impact.AggregationInfo
with an emptyAggregationFunction
array and the main query filter. This ensures that theGroupByExecutor
will compute all the groups (from the result of applying the main query filter) but no additional unnecessary aggregation will be done.