Skip to content

ES|QL: Fix aggregation on null value#139797

Merged
ivancea merged 22 commits intoelastic:mainfrom
dimitris-athanasiou:fix-agg-on-null-esql
Jan 13, 2026
Merged

ES|QL: Fix aggregation on null value#139797
ivancea merged 22 commits intoelastic:mainfrom
dimitris-athanasiou:fix-agg-on-null-esql

Conversation

@dimitris-athanasiou
Copy link
Contributor

@dimitris-athanasiou dimitris-athanasiou commented Dec 19, 2025

This correctly handles aggregations on null values.

Aggregations on constants do not work as per #100634.

However, we can do better in terms of handling null.

We achieve this by 2 changes:

  • we do not fold aggregate functions as AggregateMapper does not handle Literal values.
  • we reuse the existing ReplaceStatsFilteredAggWithEval rule to replace aggs on null with an eval.

I have renamed ReplaceStatsFilteredAggWithEval to ReplaceStatsFilteredOrNullAggWithEval to capture the new scenario where we apply the rule.

Closes #137544
Closes #110257

This correctly handles aggregations on null values.

Aggregations on constants do not work as per elastic#100634.

However, we can do better in terms of handling null.

We achieve this by 2 changes:

  - we do not fold aggregate functions as `AggregateMapper` does not handle `Literal` values.
  - we reuse the existing `ReplaceStatsFilteredAggWithEval` rule to replace aggs on null with an eval.

I have renamed `ReplaceStatsFilteredAggWithEval` to `ReplaceStatsFilteredOrNullAggWithEval` to capture
the new scenario where we apply the rule.

Closes elastic#137544
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-analytical-engine (Team:Analytics)

@elasticsearchmachine elasticsearchmachine added Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.4.0 labels Dec 19, 2025
@elasticsearchmachine
Copy link
Collaborator

Hi @dimitris-athanasiou, I've created a changelog YAML for you.

@dimitris-athanasiou dimitris-athanasiou added auto-backport Automatically create backport pull requests when merged v9.2.4 labels Dec 19, 2025
Copy link
Contributor

@alex-spies alex-spies left a comment

Choose a reason for hiding this comment

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

Thank you @dimitris-athanasiou !

Sorry for the drive-by, but this got my attention as we've had issues with aggs on consts in the past. Not a full review, but I wanted to understand the main mechanism.

We're performing the same optimization as for STATS some_agg(...) WHERE false - which is semantically the same, as the WHERE false filter is the same as having no rows, or a single null row, being fed into the agg function. This looks correct to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

It'd be lovely if we could add a couple more tests, as we're adding something pretty new here:

  • tests with different agg functions, esp. the special ones from ReplaceStatsFilteredAggWithEval#mapNullToValue
  • tests with more than 1 agg function where 1 or more get null literals, and some where another agg function does not get a null value.
  • tests with BY
  • tests with INLINE STATS
  • tests with per-agg WHERE clauses

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tests with different agg functions, esp. the special ones from ReplaceStatsFilteredAggWithEval#mapNullToValue

There are already tests for count/count_distinct:

  • stats.countNull
  • stats.countDistinctNull

Those were both working because those two functions are not nullable and they were escaping null-folding because of that.

I'll definitely add tests for all other points you bring up!

import static org.hamcrest.Matchers.startsWith;

//@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE", reason = "debug")
@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE", reason = "debug")
Copy link
Contributor

Choose a reason for hiding this comment

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

Leftover?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, since this closes #137544, let's add the repro queries from the issue to the spec tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

Added tests for FUSE, they work correctly in this PR

@dimitris-athanasiou
Copy link
Contributor Author

I am getting test failures for the aggregation tests for test cases that contain null. I am not sure how to handle those. I need guidance for these. @bpintea Please let me know when you take a look at the PR.

@bpintea
Copy link
Contributor

bpintea commented Dec 23, 2025

@dimitris-athanasiou "in real life", a query like STATS MAX(null) is replaced by a rule called SubstituteSurrogateAggregations with MvMax(null), which has an evaluator for the NULL type (the EvalOperator.CONSTANT_NULL_FACTORY). The AbstractAggregationTestCase#resolveExpression tries to emulate that (#resolveSurrogates). But that requires a literal (like null), not a field of type NULL (which actually does not exist, everything that comes from ES will have a non-NULL type).
So I think we'll probably need to modify the test to cater for this case.

@dimitris-athanasiou
Copy link
Contributor Author

@bpintea Thanks for the explanation there!

If I get it right, would adding this at the end of resolveSurrogates() be the solution:

if (Expressions.anyMatch(expression.children(), Expressions::isGuaranteedNull)) {
    return new Literal(expression.source(), null, expression.dataType());
}

It seems to make the tests pass.

@bpintea
Copy link
Contributor

bpintea commented Dec 29, 2025

If I get it right, would adding this at the end of resolveSurrogates() be the solution:

Maybe... but I feel the solution to that might need to update what we feed into the test, rather how we run the test.
I'd think that resolveSurrogates() shouldn't be changed if we can avoid passing DataType.NULL fields into it.

@elasticsearchmachine
Copy link
Collaborator

Hi @dimitris-athanasiou, I've updated the changelog YAML for you.

Copy link
Contributor

@ivancea ivancea left a comment

Choose a reason for hiding this comment

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

LGTM!
As discussed, I'll continue the work on this PR, as it became a blocker for #139417
Mostly, fix the unit tests on nulls, add some extra tests, and review the other comments. And whatever comes from those changes

@ivancea ivancea merged commit e3e29d3 into elastic:main Jan 13, 2026
34 of 35 checks passed
@elasticsearchmachine
Copy link
Collaborator

💔 Backport failed

You can use sqren/backport to manually backport by running backport --upstream elastic/elasticsearch --pr 139797

ivancea pushed a commit to ivancea/elasticsearch that referenced this pull request Jan 13, 2026
@ivancea ivancea removed the v9.2.5 label Jan 13, 2026
elasticsearchmachine pushed a commit that referenced this pull request Jan 13, 2026
ivancea added a commit that referenced this pull request Jan 14, 2026
ivancea added a commit that referenced this pull request Jan 14, 2026
Continuation of #139797, adding more tests for timeseries
ivancea added a commit to ivancea/elasticsearch that referenced this pull request Jan 14, 2026
Continuation of elastic#139797, adding more tests for timeseries
@ivancea ivancea added v9.3.0 and removed v9.3.1 labels Jan 14, 2026
alex-spies added a commit to alex-spies/elasticsearch that referenced this pull request Jan 14, 2026
eranweiss-elastic pushed a commit to eranweiss-elastic/elasticsearch that referenced this pull request Jan 15, 2026
spinscale pushed a commit to spinscale/elasticsearch that referenced this pull request Jan 21, 2026
spinscale pushed a commit to spinscale/elasticsearch that referenced this pull request Jan 21, 2026
spinscale pushed a commit to spinscale/elasticsearch that referenced this pull request Jan 21, 2026
Continuation of elastic#139797, adding more tests for timeseries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL auto-backport Automatically create backport pull requests when merged >bug Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.3.0 v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ES|QL] FUSE command fails when a column contains NULL ESQL: FoldNull folding aggs into literals, raising an "unknown agg" error

5 participants