Skip to content

ESQL: Fix INLINE STATS GROUP BY null being incorrectly pruned (#140027)#141095

Merged
elasticsearchmachine merged 1 commit intoelastic:9.3from
astefan:140027_9.3_backport
Jan 22, 2026
Merged

ESQL: Fix INLINE STATS GROUP BY null being incorrectly pruned (#140027)#141095
elasticsearchmachine merged 1 commit intoelastic:9.3from
astefan:140027_9.3_backport

Conversation

@astefan
Copy link
Contributor

@astefan astefan commented Jan 22, 2026

Backports #140027

…tic#140027)

This PR fixes the issue where `INLINE STATS GROUP BY null` was being
incorrectly pruned by `PruneLeftJoinOnNullMatchingField`.

Fixes elastic#139887

## Problem For query:

```
FROM employees
| INLINE STATS c = COUNT(*) BY n = null
| KEEP c, n
| LIMIT 3
```

During `LogicalPlanOptimizer`:

```
Limit[3[INTEGER],false,false]
\_EsqlProject[[c{r}elastic#2, n{r}elastic#4]]
  \_InlineJoin[LEFT,[n{r}elastic#4],[n{r}elastic#4]]
    |_Eval[[null[NULL] AS n#4]]
    | \_EsRelation[employees][<no-fields>{r$}elastic#7]
    \_Aggregate[[n{r}elastic#4],[COUNT(*[KEYWORD],true[BOOLEAN],PT0S[TIME_DURATION]) AS c#2, n{r}elastic#4]]
      \_StubRelation[[<no-fields>{r$}elastic#7, n{r}elastic#4]]
```

The following join node:

```
InlineJoin[LEFT,[n{r}elastic#4],[n{r}elastic#4]]
|_Eval[[null[NULL] AS n#4]]
| \_EsRelation[employees][<no-fields>{r$}elastic#7]
\_Aggregate[[n{r}elastic#4],[COUNT(*[KEYWORD],true[BOOLEAN],PT0S[TIME_DURATION]) AS c#2, n{r}elastic#4]]
  \_StubRelation[[<no-fields>{r$}elastic#7, n{r}elastic#4]]
```

should NOT have `PruneLeftJoinOnNullMatchingField` applied, because the
right side is an `Aggregate` (originating from `INLINE STATS`). Since
`STATS` supports `GROUP BY null`, the join key being null is a valid use
case. Pruning this join would incorrectly eliminate the aggregation
results, changing the query semantics.

During `LocalLogicalPlanOptimizer`:

```
ProjectExec[[c{r}elastic#2, n{r}elastic#4]]
\_LimitExec[3[INTEGER],null]
  \_ExchangeExec[[c{r}elastic#2, n{r}elastic#4],false]
    \_FragmentExec[filter=null, estimatedRowSize=0, reducer=[], fragment=[<>
Project[[c{r}elastic#2, n{r}elastic#4]]
\_Limit[3[INTEGER],false,false]
  \_InlineJoin[LEFT,[n{r}elastic#4],[n{r}elastic#4]]
    |_Eval[[null[NULL] AS n#4]]
    | \_EsRelation[employees][<no-fields>{r$}elastic#7]
    \_LocalRelation[[c{r}elastic#2, n{r}elastic#4],Page{blocks=[LongVectorBlock[vector=ConstantLongVector[positions=1, value=100]], ConstantNullBlock[positions=1]]}]<>]]
```

The following join node:

```
InlineJoin[LEFT,[n{r}elastic#4],[n{r}elastic#4]]
|_Eval[[null[NULL] AS n#4]]
| \_EsRelation[employees][<no-fields>{r$}elastic#7]
\_LocalRelation[[c{r}elastic#2, n{r}elastic#4],Page{blocks=[LongVectorBlock[vector=ConstantLongVector[positions=1, value=100]], ConstantNullBlock[positions=1]]}]
```

should NOT have `PruneLeftJoinOnNullMatchingField` applied, because the
right side is a `LocalRelation` (the `Aggregate` was optimized into a
`LocalRelation` containing the pre-computed aggregation results).
Pruning this join when the join key is null would discard the valid
aggregation results stored in the `LocalRelation`, incorrectly producing
null values instead of the expected count.

## Solution The fix ensures that `PruneLeftJoinOnNullMatchingField` only
applies to `LOOKUP JOIN` nodes, where `join.right()` is an `EsRelation`.
For `INLINE STATS` joins, the right side can be:

 - `Aggregate` (before optimization), or
 - `LocalRelation` (after the aggregate is optimized)

By checking `join.right() instanceof EsRelation`, we correctly skip the
pruning optimization for `INLINE STATS` joins, preserving the expected
query results when grouping by null.

(cherry picked from commit f3ccb70)
@astefan astefan added >bug backport external-contributor Pull request authored by a developer outside the Elasticsearch team :Analytics/ES|QL AKA ESQL v9.3.1 auto-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) and removed external-contributor Pull request authored by a developer outside the Elasticsearch team labels Jan 22, 2026
@elasticsearchmachine elasticsearchmachine merged commit 2aabec1 into elastic:9.3 Jan 22, 2026
35 checks passed
@astefan astefan deleted the 140027_9.3_backport branch January 22, 2026 13:47
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-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) backport >bug v9.3.1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants