Skip to content

Conversation

@astefan
Copy link
Contributor

@astefan astefan commented Sep 18, 2025

This PR covers a small range of edge cases where the logical optimization rules "compete" in optimizing aggregations to a simplified tree that is either a LocalRelation by itself or that have the source leaf node as LocalRelation. When the right-hand side becomes a non-concrete source, it is one of two situations now:

  • the EsqlSession ran the left-hand side query and the results are modeled as LocalRelation which replace the StubRelation of the right-hand side
  • the logical optimization flow optimized away the right-hand side (essentially transformed it into a local query, no ES data involved)

The code in the PR covers the second scenario above. There are few rules which touch the idea of inline stats and which optimize away the right-hand side, but there are also others which are generic and have no different behavior for inline stats.

  • PruneEmptyPlans is one rule where I debated a lot if it should also deal with StubRelation as an "empty" plan or remain as is. I decided that the particularities of inlinejoin are not the concern of this rule, but those of the EsqlSession where the "coordination" of the inlinejoin is mostly happening.
  • PruneColumns is another rule where the pruning of the right-hand side is handled
  • lastly, ReplaceStatsFilteredAggWithEval deals with pruning inline stats and stats that have filters that can also be pruned

@elasticsearchmachine
Copy link
Collaborator

Hi @astefan, I've created a changelog YAML for you.

@astefan astefan marked this pull request as ready for review September 19, 2025 08:26
@elasticsearchmachine
Copy link
Collaborator

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

@elasticsearchmachine elasticsearchmachine added the Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) label Sep 19, 2025
private static LogicalPlan emptyLocalRelation(UnaryPlan plan) {
// create an empty local relation with no attributes
return new LocalRelation(plan.source(), plan.output(), EmptyLocalSupplier.EMPTY);
return skipPlan(plan);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unrelated to the meat of the PR

Copy link
Contributor

@bpintea bpintea left a comment

Choose a reason for hiding this comment

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

LG, not sure about the source of the failure, but it seems it might have a different origin? Happy to re-review otherwise.

Comment on lines 155 to 156
} else if (ij.right() instanceof LocalRelation && skipped == null
|| ij.right() instanceof LocalRelation == false && ij.right().anyMatch(p -> p instanceof LocalRelation)) {
Copy link
Contributor

@bpintea bpintea Sep 22, 2025

Choose a reason for hiding this comment

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

Could this be:

Suggested change
} else if (ij.right() instanceof LocalRelation && skipped == null
|| ij.right() instanceof LocalRelation == false && ij.right().anyMatch(p -> p instanceof LocalRelation)) {
} else if (ij.right().children().stream().anyMatch(c -> c.anyMatch(LocalRelation.class::isInstance))) {

Then I think you wouldn't need to pass the extra skipped param.
Which, otherwise, could be a bool, maybe, since it's only used in a test against null.

Comment on lines +160 to +162
var p = ij.right();
p.setOptimized();
subPlan.set(new LogicalPlanTuple(p, ij.right()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't know if it'd be worth creating a light static method to do this here and the similar thing above.

@astefan
Copy link
Contributor Author

astefan commented Sep 23, 2025

@bpintea thank you for having a look. I have added a more detailed description to the PR, trying to clarify the situations (conceptually) this PR is trying to address and why this particular fix has been chosen. Hope this helps.

@bpintea bpintea self-requested a review September 25, 2025 14:30
import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin;
import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation;

public final class PruneEmptyInlineStatsRightSide extends OptimizerRules.OptimizerRule<InlineJoin> {
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a PruneLeftJoinOnNullMatchingField. Should we call this some similar way? (maybe PruneInlineJoinOnEmptyRightSide -- I guess we could use InlineJoin instead of InlineStats, since the rule should apply correctly even if we found other uses of InlineJoin).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, makes sense. I've renamed it.

Comment on lines +156 to +158
} else if (ij.right() instanceof LocalRelation relation
&& (subPlansResults.isEmpty() || subPlansResults.contains(relation) == false)
|| ij.right() instanceof LocalRelation == false && ij.right().anyMatch(p -> p instanceof LocalRelation)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth extracting this check into a method, it's a bit hard to read.
I tried to simplify it a bit, but we run into block tracking issues, so I guess this could probably be simplified, with some extra accounting.
But that can be a followup.

@astefan astefan added the auto-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) label Sep 25, 2025
@elasticsearchmachine elasticsearchmachine merged commit 9dfc8d4 into elastic:main Sep 26, 2025
34 checks passed
@astefan astefan deleted the handle_right_LocalRelation branch September 26, 2025 06:36
@alex-spies
Copy link
Contributor

Late to the party, but just wanted to say this looks right to me. Thanks for keeping me in the loop, the inline stats machinery and how it evolves is super interesting!

szybia added a commit to szybia/elasticsearch that referenced this pull request Sep 26, 2025
…-dls

* upstream/main: (55 commits)
  Mute org.elasticsearch.upgrades.MatchOnlyTextRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135525
  Address es819 tsdb doc values format performance bug (elastic#135505)
  Remove obsolete --add-opens from JDK API extractor tool (elastic#135445)
  [CI] Fix MergeWithFailureIT (elastic#135447)
  Increase wait time in AdaptiveAllocationsScalerServiceTests (elastic#135510)
  ES|QL: Handle multi values in FUSE (elastic#135448)
  Mute org.elasticsearch.upgrades.SyntheticSourceRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135512
  Add trust configuration for cross cluster api keys (elastic#134893)
  ESQL: Fix flakiness in SessionUtilsTests (elastic#135375)
  Mute org.elasticsearch.upgrades.LogsdbIndexingRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135511
  Mute org.elasticsearch.upgrades.MatchOnlyTextRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135325
  Require all functions to provide examples (elastic#135094)
  Mute org.elasticsearch.upgrades.SyntheticSourceRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135344
  Mute org.elasticsearch.upgrades.TextRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135236
  Mute org.elasticsearch.upgrades.TextRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135238
  Mute org.elasticsearch.upgrades.LogsdbIndexingRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135327
  Mute org.elasticsearch.upgrades.MatchOnlyTextRollingUpgradeIT testIndexing {upgradedNodes=3} elastic#135324
  Mute org.elasticsearch.upgrades.StandardToLogsDbIndexModeRollingUpgradeIT testLogsIndexing {upgradedNodes=3} elastic#135315
  ESQL: Handle right hand side of Inline Stats coming optimized with LocalRelation shortcut (elastic#135011)
  Mute org.elasticsearch.upgrades.TextRollingUpgradeIT testIndexing {upgradedNodes=3} elastic#135237
  ...
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!) >bug Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants