-
Notifications
You must be signed in to change notification settings - Fork 25.6k
ES|QL: Remove redundant sorts from execution plan #121156
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
Changes from 7 commits
4910db5
a0b463a
78c86de
10117dc
4d623d7
0ec1539
7ec3534
937e0dc
3569a16
df50502
3af3c11
16bfba0
5c1ed70
c73246d
1c260e1
a5d30ef
f881d2d
319cc69
7a9c8ab
2d34e50
f2af414
2303d8b
9b22923
9083477
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| pr: 121156 | ||
| summary: Remove redundant sorts from execution plan | ||
| area: ES|QL | ||
| type: bug | ||
| issues: [] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| package org.elasticsearch.xpack.esql.optimizer.rules.logical; | ||
|
|
||
| import org.elasticsearch.xpack.esql.plan.logical.Dissect; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Enrich; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Filter; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Grok; | ||
| import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; | ||
| import org.elasticsearch.xpack.esql.plan.logical.MvExpand; | ||
| import org.elasticsearch.xpack.esql.plan.logical.OrderBy; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Project; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Rename; | ||
| import org.elasticsearch.xpack.esql.plan.logical.TopN; | ||
| import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan; | ||
| import org.elasticsearch.xpack.esql.plan.logical.join.Join; | ||
|
|
||
| /** | ||
| * SORT cannot be executed without a LIMIT, as ES|QL doesn't support unbounded sort (yet). | ||
| * | ||
| * The planner tries to push down LIMIT and transform all the unbounded sorts into a TopN. | ||
| * In some cases it's not possible though, eg. | ||
| * | ||
| * from test | sort x | lookup join lookup on x | sort y | ||
| * | ||
| * from test | sort x | mv_expand x | sort y | ||
| * | ||
| * "sort y" will become a TopN, but "sort x" will remain unbounded, so the query could not be executed. | ||
| * | ||
| * In most cases though, last SORT make the previous SORTs redundant, | ||
| * ie. it will re-sort previously sorted results | ||
| * often with a different order. | ||
| * | ||
| * This rule finds and removes redundant SORTs, making the plan executable. | ||
| */ | ||
| public class RemoveRedundantSort extends OptimizerRules.OptimizerRule<TopN> { | ||
|
|
||
| @Override | ||
| protected LogicalPlan rule(TopN plan) { | ||
| OrderBy redundant = findRedundantSort(plan); | ||
| if (redundant == null) { | ||
| return plan; | ||
| } | ||
| return plan.transformDown(p -> { | ||
| if (p == redundant) { | ||
| return redundant.child(); | ||
| } | ||
| return p; | ||
| }); | ||
|
||
| } | ||
|
|
||
| private OrderBy findRedundantSort(TopN plan) { | ||
|
||
| LogicalPlan p = plan.child(); | ||
| while (true) { | ||
| if (p instanceof OrderBy ob) { | ||
| return ob; | ||
| } | ||
| if (p instanceof UnaryPlan unary) { | ||
| if (unary instanceof Filter | ||
| || unary instanceof Project | ||
| || unary instanceof Rename | ||
| || unary instanceof MvExpand | ||
| || unary instanceof Enrich | ||
| || unary instanceof Grok | ||
| || unary instanceof Dissect | ||
| // If we introduce window functions, the previous sort could actually become relevant | ||
| // so to be sure we don't introduce regressions, we'll have to exclude places where these functions could be used | ||
| // || unary instanceof Eval | ||
| // || unary instanceof Aggregate | ||
| ) { | ||
| p = unary.child(); | ||
| continue; | ||
| } | ||
| } else if (p instanceof Join lj) { | ||
| p = lj.left(); | ||
| // TODO do it also on the right-hand side? | ||
| continue; | ||
|
||
| } | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| import org.elasticsearch.common.io.stream.StreamInput; | ||
| import org.elasticsearch.common.io.stream.StreamOutput; | ||
| import org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware; | ||
| import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware; | ||
| import org.elasticsearch.xpack.esql.capabilities.TelemetryAware; | ||
| import org.elasticsearch.xpack.esql.common.Failures; | ||
| import org.elasticsearch.xpack.esql.core.capabilities.Resolvables; | ||
|
|
@@ -25,7 +26,7 @@ | |
|
|
||
| import static org.elasticsearch.xpack.esql.common.Failure.fail; | ||
|
|
||
| public class OrderBy extends UnaryPlan implements PostAnalysisVerificationAware, TelemetryAware { | ||
| public class OrderBy extends UnaryPlan implements PostAnalysisVerificationAware, PostOptimizationVerificationAware, TelemetryAware { | ||
| public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(LogicalPlan.class, "OrderBy", OrderBy::new); | ||
|
|
||
| private final List<Order> order; | ||
|
|
@@ -109,4 +110,9 @@ public void postAnalysisVerification(Failures failures) { | |
| } | ||
| }); | ||
| } | ||
|
|
||
| @Override | ||
| public void postOptimizationVerification(Failures failures) { | ||
| failures.add(fail(this, "The query cannot be executed because it would require unbounded sort")); | ||
|
||
| } | ||
| } | ||
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.
It makes sense to combine this with
PruneOrderByBeforeStatssince the logic is similar.Remove -> Prune
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.
Thanks Costin, I didn't realize the two rules were so similar.
I'll merge them in a single one.
A detail worth noting is that
PruneOrderByBeforeStatscurrently considersEvalas a sort-agnostic plan (and it's correct now, since we don't have window functions yet). I'll keep the same logic for now, and I'll add a comment so that we don't forget.The good thing is that, with this logic, we allow SORT pruning after all the currently supported plans (apart from LIMIT, but it will become a TopN anyway), so now we no longer have unbounded sort.