Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4910db5
Remove redundant sorts from execution plan
luigidellaquila Jan 29, 2025
a0b463a
Update docs/changelog/121156.yaml
luigidellaquila Jan 29, 2025
78c86de
More tests
luigidellaquila Jan 29, 2025
10117dc
Merge remote-tracking branch 'luigidellaquila/esql/remove_redundant_s…
luigidellaquila Jan 29, 2025
4d623d7
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Jan 30, 2025
0ec1539
Verify OrderBy after optimization
luigidellaquila Jan 30, 2025
7ec3534
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Jan 30, 2025
937e0dc
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Jan 30, 2025
3569a16
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Jan 31, 2025
df50502
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Feb 3, 2025
3af3c11
Implement review suggestions
luigidellaquila Feb 4, 2025
16bfba0
Merge remote-tracking branch 'luigidellaquila/esql/remove_redundant_s…
luigidellaquila Feb 4, 2025
5c1ed70
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Feb 4, 2025
c73246d
Remove AddDefaultTopN rule
luigidellaquila Feb 4, 2025
1c260e1
More tests
luigidellaquila Feb 6, 2025
a5d30ef
Better error message
luigidellaquila Feb 6, 2025
f881d2d
Delete OrderExec
luigidellaquila Feb 6, 2025
319cc69
Add SortAware interface
luigidellaquila Feb 6, 2025
7a9c8ab
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Feb 6, 2025
2d34e50
Better description for SortAware
luigidellaquila Feb 7, 2025
f2af414
More tests
luigidellaquila Feb 7, 2025
2303d8b
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Feb 10, 2025
9b22923
SortAware -> SortAgnostic and simplify
luigidellaquila Feb 10, 2025
9083477
Merge branch 'main' into esql/remove_redundant_sort
luigidellaquila Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/121156.yaml
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: []
2 changes: 0 additions & 2 deletions muted-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,6 @@ tests:
- class: org.elasticsearch.xpack.security.profile.ProfileIntegTests
method: testSetEnabled
issue: https://github.com/elastic/elasticsearch/issues/121183
- class: org.elasticsearch.xpack.esql.optimizer.LogicalPlanOptimizerTests
issue: https://github.com/elastic/elasticsearch/issues/121185
- class: org.elasticsearch.xpack.security.CoreWithSecurityClientYamlTestSuiteIT
method: test {yaml=cat.aliases/10_basic/Simple alias}
issue: https://github.com/elastic/elasticsearch/issues/121186
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1346,3 +1346,50 @@ language_code:integer | language_name:keyword | country:text
1 | English | United States of America
1 | English | null
;


sortBeforeAndAfterJoin
required_capability: join_lookup_v12
required_capability: remove_redundant_sort

FROM employees
| sort first_name
| EVAL language_code = languages
| LOOKUP JOIN languages_lookup ON language_code
| WHERE emp_no >= 10091 AND emp_no < 10094
| SORT emp_no
| KEEP emp_no, language_code, language_name
;

emp_no:integer | language_code:integer | language_name:keyword
10091 | 3 | Spanish
10092 | 1 | English
10093 | 3 | Spanish
;



sortBeforeAndAfterMultipleJoinAndMvExpand
required_capability: join_lookup_v12
required_capability: remove_redundant_sort

FROM employees
| sort first_name
| EVAL language_code = languages
| LOOKUP JOIN languages_lookup ON language_code
| WHERE emp_no >= 10091 AND emp_no < 10094
| SORT language_name
| MV_EXPAND first_name
| SORT first_name
| MV_EXPAND last_name
| SORT last_name
| LOOKUP JOIN languages_lookup ON language_code
| SORT emp_no
| KEEP emp_no, language_code, language_name
;

emp_no:integer | language_code:integer | language_name:keyword
10091 | 3 | Spanish
10092 | 1 | English
10093 | 3 | Spanish
;
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,17 @@ from employees | where emp_no == 10003 | mv_expand first_name | keep first_name
first_name:keyword
Parto
;


sortBeforeAndAfterMvExpand
from employees
| sort first_name
| mv_expand job_positions
| sort emp_no, job_positions
| keep emp_no, job_positions
| limit 2;

emp_no:integer | job_positions:keyword
10001 | Accountant
10001 | Senior Python Developer
;
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,14 @@ public enum Cap {
/**
* Support for aggregate_metric_double type
*/
AGGREGATE_METRIC_DOUBLE;
AGGREGATE_METRIC_DOUBLE,

/**
* Fix for https://github.com/elastic/elasticsearch/issues/120817
* and https://github.com/elastic/elasticsearch/issues/120803
* Support for queries that have multiple SORTs that cannot become TopN
*/
REMOVE_REDUNDANT_SORT;

private final boolean enabled;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PushDownEnrich;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PushDownEval;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PushDownRegexExtract;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.RemoveRedundantSort;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.RemoveStatsOverride;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceAggregateAggExpressionWithEval;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceAggregateNestedExpressionWithEval;
Expand Down Expand Up @@ -195,6 +196,6 @@ protected static Batch<LogicalPlan> operators() {
}

protected static Batch<LogicalPlan> cleanup() {
return new Batch<>("Clean Up", new ReplaceLimitAndSortAsTopN(), new ReplaceRowAsLocalRelation());
return new Batch<>("Clean Up", new ReplaceLimitAndSortAsTopN(), new ReplaceRowAsLocalRelation(), new RemoveRedundantSort());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public Failures verify(LogicalPlan plan) {
PlanConsistencyChecker.checkPlan(p, dependencyFailures);

if (failures.hasFailures() == false) {
if (p instanceof PostOptimizationVerificationAware pova) {
pova.postOptimizationVerification(failures);
}
p.forEachExpression(ex -> {
if (ex instanceof PostOptimizationVerificationAware va) {
va.postOptimizationVerification(failures);
Expand Down
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> {
Copy link
Member

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 PruneOrderByBeforeStats since the logic is similar.
Remove -> Prune

Copy link
Contributor Author

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 PruneOrderByBeforeStats currently considers Eval as 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.


@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;
});
Copy link
Member

Choose a reason for hiding this comment

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

A bottom up traversal would help collect all pruned sorts in one traversal instead of a top-down per sort:

  • those that occur before stats
  • those that occur before other sorts

}

private OrderBy findRedundantSort(TopN plan) {
Copy link
Member

Choose a reason for hiding this comment

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

Return a list to perform only one modification (and traversals) on the tree.

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;
Copy link
Member

Choose a reason for hiding this comment

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

Make the function recursive and let it run on both sides.

}
return null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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"));
Copy link
Member

Choose a reason for hiding this comment

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

I'd rephrase the error message to include an action item for the user: `Unbounded sort not supported yet, please add a limit"

}
}
Loading