ESQL: Introduce per agg filter#113735
Conversation
| : (qualifiedName ASSIGN)? booleanExpression | ||
| ; | ||
|
|
||
| fromCommand | ||
| : FROM indexPattern (COMMA indexPattern)* metadata? | ||
| ; | ||
|
|
||
| indexPattern | ||
| : clusterString COLON indexString | ||
| | indexString | ||
| : (clusterString COLON)? indexString |
There was a problem hiding this comment.
Small improvements to the grammar that don't change the parsing.
| // unwrap filtered expression | ||
| if (e instanceof FilteredExpression fe) { | ||
| e = fe.delegate(); | ||
| // TODO add verification for filter clause | ||
| } |
| this( | ||
| Source.readFrom((PlanStreamInput) in), | ||
| in.readNamedWriteable(Expression.class), | ||
| in.getTransportVersion().onOrAfter(TransportVersions.ESQL_PER_AGGREGATE_FILTER) | ||
| ? in.readNamedWriteable(Expression.class) | ||
| : Literal.TRUE, | ||
| in.getTransportVersion().onOrAfter(TransportVersions.ESQL_PER_AGGREGATE_FILTER) | ||
| ? in.readNamedWriteableCollectionAsList(Expression.class) | ||
| : emptyList() | ||
| ); |
There was a problem hiding this comment.
Uniform serialization across all aggregate functions - we should had that since the beginning to avoid having each subclass add its own repetitive serialization.
/cc @nik9000
There was a problem hiding this comment.
I think it's easier to make the filter wrap the aggregation rather than inside each one. I think also that serializing a list in case aggs use it is probably not a great choice either. It's fine, but feels like something we'd do when we need it.
| return NodeInfo.create(this, Avg::new, field(), filter()); | ||
| } | ||
|
|
||
| @Override | ||
| public Avg replaceChildren(List<Expression> newChildren) { | ||
| return new Avg(source(), newChildren.get(0)); | ||
| return new Avg(source(), newChildren.get(0), newChildren.get(1)); | ||
| } | ||
|
|
||
| @Override | ||
| public Avg withFilter(Expression filter) { | ||
| return new Avg(source(), field(), filter); |
There was a problem hiding this comment.
The side effect is all aggs had to be modified to take into account the parent filter.
There was a problem hiding this comment.
Could we wrap them with something like we do at runtime? It feels like that might work.
There was a problem hiding this comment.
Not sure what you mean. My initial approach was to wrap the agg and filter in separate class (FilteredAggregate with two children - aggregate and filter/expression ) however this messed up the optimizer since the rules picked the aggregate functions but had no idea there was a filter associated with it.
This is problematic for scenario where the same agg is defined with different filter:
STATS c = count(). c_f = count() WHERE a > 1
since the where filter is disjoined from the count, the rules replaced the second count with the first one but that is incorrect since they have different execution paths.
Furthermore at compute time the filter and actual agg gets fused anyway so the two are a pair anyway.
There was a problem hiding this comment.
They do get fused anyway, but it's just a lot more convenient if every agg doesn't have to worry about being filtered. If filter is a member here we have to think about it all over the place. It's enough that the fused filter at compute time has to add all the tests for it, I'd love to avoid that bubbling all the way out. OTOH, if that's how it has to be, I'm not deep enough to object.
There was a problem hiding this comment.
I wonder if this means each aggregation evaluates its own filter? If so, from performance perspective, one of the advantages of separating filter from aggregation is that, if the same filter applies to different aggregations, it makes it easier to recognize that it is the same filter and can be evaluated once for all aggregations that share it, in stead of evaluating each filter within each aggregation separately. However I couldn’t figure out how the current infrastructure supports it yet. At first look at this, it reminds me of the (common)table expressions and union all subqueries in SQL, but we don’t have this infrastructure in ES|QL yet, and it could be a much broader scope.
There was a problem hiding this comment.
This approach doesn't prevent optimizing the filter evaluation.
Each aggregation ends up having its own filter because count(*) is different than count(*) where a > 10. Initially they were decoupled however the rules considered them the same since the filter was sitting outside the aggregation.
When dealing with the same filter, we can assemble a different pipeline so the mask or masked block is reused across multiple aggregations.
| for (int i = 0, s = aggs.size(); i < s; i++) { | ||
| NamedExpression agg = aggs.get(i); | ||
|
|
There was a problem hiding this comment.
This needs to be rolled back.
...l/src/main/java/org/elasticsearch/xpack/esql/planner/AbstractPhysicalOperationProviders.java
Show resolved
Hide resolved
costin
left a comment
There was a problem hiding this comment.
Draft PR to check the CI.
Was based on an old commit to avoid some of the volatility in main.
Will rebase and polish after a few CI rounds.
60af98d to
d2db8db
Compare
d2db8db to
8a201ec
Compare
|
Pinging @elastic/es-analytical-engine (Team:Analytics) |
|
Pinging @elastic/kibana-esql (ES|QL-ui) |
Add support for aggregation scoped filters that work dynamically on the
data in each group.
| STATS
success = COUNT(*) WHERE 200 <= code AND code < 300,
redirect = COUNT(*) WHERE 300 <= code AND code < 400,
client_err = COUNT(*) WHERE 400 <= code AND code < 500,
server_err = COUNT(*) WHERE 500 <= code AND code < 600,
total_count = COUNT(*)
Implementation wise, the base AggregateFunction has been extended to
allow a filter to be passed on. This is required to incorporate the
filter as part of the aggregate equality/identify which would fail with
the filter as an external component.
As part of the process, the serialization for the existing aggregations
had to be fixed so AggregateFunction implementations so that it
delegates to their parent first.
8a201ec to
839dbbf
Compare
costin
left a comment
There was a problem hiding this comment.
Addressed TBDs, add verifier code thus promoted from draft and rebased on latest main.
|
Hi @costin, I've created a changelog YAML for you. Note that since this PR is labelled |
💔 Backport failedThe backport operation could not be completed due to the following error: You can use sqren/backport to manually backport by running |
Add support for aggregation scoped filters that work dynamically on the
data in each group.
| STATS
success = COUNT(*) WHERE 200 <= code AND code < 300,
redirect = COUNT(*) WHERE 300 <= code AND code < 400,
client_err = COUNT(*) WHERE 400 <= code AND code < 500,
server_err = COUNT(*) WHERE 500 <= code AND code < 600,
total_count = COUNT(*)
Implementation wise, the base AggregateFunction has been extended to
allow a filter to be passed on. This is required to incorporate the
filter as part of the aggregate equality/identify which would fail with
the filter as an external component.
As part of the process, the serialization for the existing aggregations
had to be fixed so AggregateFunction implementations so that it
delegates to their parent first.
Add support for aggregation scoped filters that work dynamically on the
data in each group.
| STATS
success = COUNT(*) WHERE 200 <= code AND code < 300,
redirect = COUNT(*) WHERE 300 <= code AND code < 400,
client_err = COUNT(*) WHERE 400 <= code AND code < 500,
server_err = COUNT(*) WHERE 500 <= code AND code < 600,
total_count = COUNT(*)
Implementation wise, the base AggregateFunction has been extended to
allow a filter to be passed on. This is required to incorporate the
filter as part of the aggregate equality/identify which would fail with
the filter as an external component.
As part of the process, the serialization for the existing aggregations
had to be fixed so AggregateFunction implementations so that it
delegates to their parent first.
(cherry picked from commit d102659)
Remove dev flag left in grammar for agg filtering Related to elastic#113735
* ESQL: Introduce per agg filter (#113735) Add support for aggregation scoped filters that work dynamically on the data in each group. | STATS success = COUNT(*) WHERE 200 <= code AND code < 300, redirect = COUNT(*) WHERE 300 <= code AND code < 400, client_err = COUNT(*) WHERE 400 <= code AND code < 500, server_err = COUNT(*) WHERE 500 <= code AND code < 600, total_count = COUNT(*) Implementation wise, the base AggregateFunction has been extended to allow a filter to be passed on. This is required to incorporate the filter as part of the aggregate equality/identify which would fail with the filter as an external component. As part of the process, the serialization for the existing aggregations had to be fixed so AggregateFunction implementations so that it delegates to their parent first. (cherry picked from commit d102659) * Update docs/changelog/114842.yaml * Delete docs/changelog/114842.yaml
Remove dev flag left in grammar for agg filtering Related to #113735
Remove dev flag left in grammar for agg filtering Related to elastic#113735 (cherry picked from commit 5e59ab5)
Add support for aggregation scoped filters that work dynamically on the
data in each group.
| STATS
success = COUNT(*) WHERE 200 <= code AND code < 300,
redirect = COUNT(*) WHERE 300 <= code AND code < 400,
client_err = COUNT(*) WHERE 400 <= code AND code < 500,
server_err = COUNT(*) WHERE 500 <= code AND code < 600,
total_count = COUNT(*)
Implementation wise, the base AggregateFunction has been extended to
allow a filter to be passed on. This is required to incorporate the
filter as part of the aggregate equality/identify which would fail with
the filter as an external component.
As part of the process, the serialization for the existing aggregations
had to be fixed so AggregateFunction implementations so that it
delegates to their parent first.
Remove dev flag left in grammar for agg filtering Related to elastic#113735
When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in elastic#113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
) When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
elastic#142385) When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in elastic#113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
) (#142487) When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
elastic#142385) (elastic#142487) When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in elastic#113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
) (#142487) (#142533) When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
Add support for aggregation scoped filters that work dynamically on the
data in each group.
Implementation wise, the base AggregateFunction has been extended to
allow a filter to be passed on. This is required to incorporate the
filter as part of the aggregate equality/identify which would fail with
the filter as an external component.
As part of the process, the serialization for the existing aggregations
had to be fixed so AggregateFunction implementations so that it
delegates to their parent first.