Skip to content

Commit d5d7937

Browse files
ES|QL: Remove redundant sorts from execution plan (#121156) (#122187)
1 parent e54acc3 commit d5d7937

30 files changed

+679
-359
lines changed

docs/changelog/121156.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 121156
2+
summary: Remove redundant sorts from execution plan
3+
area: ES|QL
4+
type: bug
5+
issues: []

x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,3 +1346,50 @@ language_code:integer | language_name:keyword | country:text
13461346
1 | English | United States of America
13471347
1 | English | null
13481348
;
1349+
1350+
1351+
sortBeforeAndAfterJoin
1352+
required_capability: join_lookup_v12
1353+
required_capability: remove_redundant_sort
1354+
1355+
FROM employees
1356+
| sort first_name
1357+
| EVAL language_code = languages
1358+
| LOOKUP JOIN languages_lookup ON language_code
1359+
| WHERE emp_no >= 10091 AND emp_no < 10094
1360+
| SORT emp_no
1361+
| KEEP emp_no, language_code, language_name
1362+
;
1363+
1364+
emp_no:integer | language_code:integer | language_name:keyword
1365+
10091 | 3 | Spanish
1366+
10092 | 1 | English
1367+
10093 | 3 | Spanish
1368+
;
1369+
1370+
1371+
1372+
sortBeforeAndAfterMultipleJoinAndMvExpand
1373+
required_capability: join_lookup_v12
1374+
required_capability: remove_redundant_sort
1375+
1376+
FROM employees
1377+
| sort first_name
1378+
| EVAL language_code = languages
1379+
| LOOKUP JOIN languages_lookup ON language_code
1380+
| WHERE emp_no >= 10091 AND emp_no < 10094
1381+
| SORT language_name
1382+
| MV_EXPAND first_name
1383+
| SORT first_name
1384+
| MV_EXPAND last_name
1385+
| SORT last_name
1386+
| LOOKUP JOIN languages_lookup ON language_code
1387+
| SORT emp_no
1388+
| KEEP emp_no, language_code, language_name
1389+
;
1390+
1391+
emp_no:integer | language_code:integer | language_name:keyword
1392+
10091 | 3 | Spanish
1393+
10092 | 1 | English
1394+
10093 | 3 | Spanish
1395+
;

x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,17 @@ from employees | where emp_no == 10003 | mv_expand first_name | keep first_name
404404
first_name:keyword
405405
Parto
406406
;
407+
408+
409+
sortBeforeAndAfterMvExpand
410+
from employees
411+
| sort first_name
412+
| mv_expand job_positions
413+
| sort emp_no, job_positions
414+
| keep emp_no, job_positions
415+
| limit 2;
416+
417+
emp_no:integer | job_positions:keyword
418+
10001 | Accountant
419+
10001 | Senior Python Developer
420+
;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,14 @@ public enum Cap {
786786
/**
787787
* Support for aggregate_metric_double type
788788
*/
789-
AGGREGATE_METRIC_DOUBLE(AGGREGATE_METRIC_DOUBLE_FEATURE_FLAG.isEnabled());
789+
AGGREGATE_METRIC_DOUBLE(AGGREGATE_METRIC_DOUBLE_FEATURE_FLAG.isEnabled()),
790+
791+
/**
792+
* Fix for https://github.com/elastic/elasticsearch/issues/120817
793+
* and https://github.com/elastic/elasticsearch/issues/120803
794+
* Support for queries that have multiple SORTs that cannot become TopN
795+
*/
796+
REMOVE_REDUNDANT_SORT;
790797

791798
private final boolean enabled;
792799

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import org.elasticsearch.xpack.esql.VerificationException;
1111
import org.elasticsearch.xpack.esql.common.Failures;
1212
import org.elasticsearch.xpack.esql.core.type.DataType;
13-
import org.elasticsearch.xpack.esql.optimizer.rules.logical.AddDefaultTopN;
1413
import org.elasticsearch.xpack.esql.optimizer.rules.logical.BooleanFunctionEqualsElimination;
1514
import org.elasticsearch.xpack.esql.optimizer.rules.logical.BooleanSimplification;
1615
import org.elasticsearch.xpack.esql.optimizer.rules.logical.CombineBinaryComparisons;
@@ -32,7 +31,7 @@
3231
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PruneEmptyPlans;
3332
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PruneFilters;
3433
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PruneLiteralsInOrderBy;
35-
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PruneOrderByBeforeStats;
34+
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PruneRedundantOrderBy;
3635
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PruneRedundantSortClauses;
3736
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PushDownAndCombineFilters;
3837
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PushDownAndCombineLimits;
@@ -116,10 +115,9 @@ protected List<Batch<LogicalPlan>> batches() {
116115

117116
protected static List<Batch<LogicalPlan>> rules() {
118117
var skip = new Batch<>("Skip Compute", new SkipQueryOnLimitZero());
119-
var defaultTopN = new Batch<>("Add default TopN", new AddDefaultTopN());
120118
var label = new Batch<>("Set as Optimized", Limiter.ONCE, new SetAsOptimized());
121119

122-
return asList(substitutions(), operators(), skip, cleanup(), defaultTopN, label);
120+
return asList(substitutions(), operators(), skip, cleanup(), label);
123121
}
124122

125123
protected static Batch<LogicalPlan> substitutions() {
@@ -189,7 +187,7 @@ protected static Batch<LogicalPlan> operators() {
189187
new PushDownRegexExtract(),
190188
new PushDownEnrich(),
191189
new PushDownAndCombineOrderBy(),
192-
new PruneOrderByBeforeStats(),
190+
new PruneRedundantOrderBy(),
193191
new PruneRedundantSortClauses()
194192
);
195193
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalVerifier.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public Failures verify(LogicalPlan plan) {
2727
PlanConsistencyChecker.checkPlan(p, dependencyFailures);
2828

2929
if (failures.hasFailures() == false) {
30+
if (p instanceof PostOptimizationVerificationAware pova) {
31+
pova.postOptimizationVerification(failures);
32+
}
3033
p.forEachExpression(ex -> {
3134
if (ex instanceof PostOptimizationVerificationAware va) {
3235
va.postOptimizationVerification(failures);

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/AddDefaultTopN.java

Lines changed: 0 additions & 54 deletions
This file was deleted.

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PruneOrderByBeforeStats.java

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.optimizer.rules.logical;
9+
10+
import org.elasticsearch.xpack.esql.plan.logical.Aggregate;
11+
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
12+
import org.elasticsearch.xpack.esql.plan.logical.OrderBy;
13+
import org.elasticsearch.xpack.esql.plan.logical.SortAgnostic;
14+
import org.elasticsearch.xpack.esql.plan.logical.TopN;
15+
import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan;
16+
17+
import java.util.ArrayDeque;
18+
import java.util.Collections;
19+
import java.util.Deque;
20+
import java.util.IdentityHashMap;
21+
import java.util.Set;
22+
23+
/**
24+
* SORT cannot be executed without a LIMIT, as ES|QL doesn't support unbounded sort (yet).
25+
* <p>
26+
* The planner tries to push down LIMIT and transform all the unbounded sorts into a TopN.
27+
* In some cases it's not possible though, eg.
28+
* <p>
29+
* from test | sort x | lookup join lookup on x | sort y
30+
* <p>
31+
* from test | sort x | mv_expand x | sort y
32+
* <p>
33+
* "sort y" will become a TopN due to the addition of the default Limit, but "sort x" will remain unbounded,
34+
* so the query could not be executed.
35+
* <p>
36+
* In most cases though, following commands can make the previous SORTs redundant,
37+
* because it will re-sort previously sorted results (eg. if there is another SORT)
38+
* or because the order will be scrambled by another command (eg. a STATS)
39+
* <p>
40+
* This rule finds and prunes redundant SORTs, attempting to make the plan executable.
41+
*/
42+
public class PruneRedundantOrderBy extends OptimizerRules.OptimizerRule<LogicalPlan> {
43+
44+
@Override
45+
protected LogicalPlan rule(LogicalPlan plan) {
46+
if (plan instanceof OrderBy || plan instanceof TopN || plan instanceof Aggregate) {
47+
Set<OrderBy> redundant = findRedundantSort(((UnaryPlan) plan).child());
48+
if (redundant.isEmpty()) {
49+
return plan;
50+
}
51+
return plan.transformDown(p -> redundant.contains(p) ? ((UnaryPlan) p).child() : p);
52+
} else {
53+
return plan;
54+
}
55+
}
56+
57+
/**
58+
* breadth-first recursion to find redundant SORTs in the children tree.
59+
* Returns an identity set (we need to compare and prune the exact instances)
60+
*/
61+
private Set<OrderBy> findRedundantSort(LogicalPlan plan) {
62+
Set<OrderBy> result = Collections.newSetFromMap(new IdentityHashMap<>());
63+
64+
Deque<LogicalPlan> toCheck = new ArrayDeque<>();
65+
toCheck.push(plan);
66+
67+
while (true) {
68+
if (toCheck.isEmpty()) {
69+
return result;
70+
}
71+
LogicalPlan p = toCheck.pop();
72+
if (p instanceof OrderBy ob) {
73+
result.add(ob);
74+
toCheck.push(ob.child());
75+
} else if (p instanceof SortAgnostic) {
76+
for (LogicalPlan child : p.children()) {
77+
toCheck.push(child);
78+
}
79+
}
80+
}
81+
}
82+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/PlanWritables.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
import org.elasticsearch.xpack.esql.plan.physical.LimitExec;
4444
import org.elasticsearch.xpack.esql.plan.physical.LocalSourceExec;
4545
import org.elasticsearch.xpack.esql.plan.physical.MvExpandExec;
46-
import org.elasticsearch.xpack.esql.plan.physical.OrderExec;
4746
import org.elasticsearch.xpack.esql.plan.physical.ProjectExec;
4847
import org.elasticsearch.xpack.esql.plan.physical.ShowExec;
4948
import org.elasticsearch.xpack.esql.plan.physical.SubqueryExec;
@@ -103,7 +102,6 @@ public static List<NamedWriteableRegistry.Entry> phsyical() {
103102
LimitExec.ENTRY,
104103
LocalSourceExec.ENTRY,
105104
MvExpandExec.ENTRY,
106-
OrderExec.ENTRY,
107105
ProjectExec.ENTRY,
108106
ShowExec.ENTRY,
109107
SubqueryExec.ENTRY,

0 commit comments

Comments
 (0)