Skip to content

Commit ef5f0de

Browse files
authored
Push down filters in logical plan (#371)
This adds a logical optimizer rule to push down the filters as much as possible. Cases where this can't be done are those where the conditions are making use of the output of aggregations or the fields define in eval. A filter rewriting rule, substituting eval's attributions in the filter and re-evaluating the filter for push'ability isn't considered here. Part of #338.
1 parent 67e23e2 commit ef5f0de

File tree

4 files changed

+310
-16
lines changed

4 files changed

+310
-16
lines changed

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

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.elasticsearch.xpack.esql.optimizer;
99

1010
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.xpack.esql.plan.logical.Eval;
1112
import org.elasticsearch.xpack.esql.plan.logical.LocalRelation;
1213
import org.elasticsearch.xpack.esql.session.EsqlSession;
1314
import org.elasticsearch.xpack.esql.session.LocalExecutable;
@@ -20,27 +21,30 @@
2021
import org.elasticsearch.xpack.ql.expression.Literal;
2122
import org.elasticsearch.xpack.ql.expression.NamedExpression;
2223
import org.elasticsearch.xpack.ql.expression.Nullability;
24+
import org.elasticsearch.xpack.ql.expression.function.aggregate.AggregateFunction;
25+
import org.elasticsearch.xpack.ql.expression.predicate.Predicates;
2326
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules;
2427
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BinaryComparisonSimplification;
2528
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanFunctionEqualsElimination;
2629
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.CombineDisjunctionsToIn;
2730
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.ConstantFolding;
2831
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.LiteralsOnTheRight;
2932
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.PruneLiteralsInOrderBy;
30-
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.PushDownAndCombineFilters;
3133
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.SetAsOptimized;
3234
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.SimplifyComparisonsArithmetics;
3335
import org.elasticsearch.xpack.ql.plan.logical.Aggregate;
3436
import org.elasticsearch.xpack.ql.plan.logical.Filter;
3537
import org.elasticsearch.xpack.ql.plan.logical.Limit;
3638
import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan;
39+
import org.elasticsearch.xpack.ql.plan.logical.OrderBy;
3740
import org.elasticsearch.xpack.ql.plan.logical.Project;
3841
import org.elasticsearch.xpack.ql.plan.logical.UnaryPlan;
3942
import org.elasticsearch.xpack.ql.rule.RuleExecutor;
4043
import org.elasticsearch.xpack.ql.type.DataTypes;
4144

4245
import java.util.ArrayList;
4346
import java.util.List;
47+
import java.util.function.Predicate;
4448

4549
import static java.util.Arrays.asList;
4650

@@ -217,4 +221,64 @@ public void execute(EsqlSession session, ActionListener<Result> listener) {
217221
}
218222
});
219223
}
224+
225+
protected static class PushDownAndCombineFilters extends OptimizerRules.OptimizerRule<Filter> {
226+
@Override
227+
protected LogicalPlan rule(Filter filter) {
228+
LogicalPlan plan = filter;
229+
LogicalPlan child = filter.child();
230+
Expression condition = filter.condition();
231+
232+
if (child instanceof Filter f) {
233+
// combine nodes into a single Filter with updated ANDed condition
234+
plan = f.with(Predicates.combineAnd(List.of(f.condition(), condition)));
235+
} else if (child instanceof UnaryPlan unary) {
236+
if (unary instanceof Aggregate agg) { // TODO: re-evaluate along with multi-value support
237+
// Only push [parts of] a filter past an agg if these/it operates on agg's grouping[s], not output.
238+
plan = maybePushDownPastUnary(
239+
filter,
240+
agg,
241+
e -> e instanceof Attribute && agg.output().contains(e) && agg.groupings().contains(e) == false
242+
|| e instanceof AggregateFunction
243+
);
244+
} else if (unary instanceof Eval eval) {
245+
// Don't push if Filter (still) contains references of Eval's fields.
246+
List<Attribute> attributes = new ArrayList<>(eval.fields().size());
247+
for (NamedExpression ne : eval.fields()) {
248+
attributes.add(ne.toAttribute());
249+
}
250+
plan = maybePushDownPastUnary(filter, eval, e -> e instanceof Attribute && attributes.contains(e));
251+
} else { // Project, OrderBy, Limit
252+
if (unary instanceof Project || unary instanceof OrderBy) {
253+
// swap the filter with its child
254+
plan = unary.replaceChild(filter.with(unary.child(), condition));
255+
}
256+
// cannot push past a Limit, this could change the tailing result set returned
257+
}
258+
}
259+
return plan;
260+
}
261+
262+
private static LogicalPlan maybePushDownPastUnary(Filter filter, UnaryPlan unary, Predicate<Expression> cannotPush) {
263+
LogicalPlan plan;
264+
List<Expression> pushable = new ArrayList<>();
265+
List<Expression> nonPushable = new ArrayList<>();
266+
for (Expression exp : Predicates.splitAnd(filter.condition())) {
267+
(exp.anyMatch(cannotPush) ? nonPushable : pushable).add(exp);
268+
}
269+
// Push the filter down even if it might not be pushable all the way to ES eventually: eval'ing it closer to the source,
270+
// potentially still in the Exec Engine, distributes the computation.
271+
if (pushable.size() > 0) {
272+
if (nonPushable.size() > 0) {
273+
Filter pushed = new Filter(filter.source(), unary.child(), Predicates.combineAnd(pushable));
274+
plan = filter.with(unary.replaceChild(pushed), Predicates.combineAnd(nonPushable));
275+
} else {
276+
plan = unary.replaceChild(filter.with(unary.child(), filter.condition()));
277+
}
278+
} else {
279+
plan = filter;
280+
}
281+
return plan;
282+
}
283+
}
220284
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,15 @@ public void testExcludeUnsupportedPattern() {
296296
""", "Cannot use field [unsupported] with unsupported type");
297297
}
298298

299+
public void testProjectAggGroupsRefs() {
300+
assertProjection("""
301+
from test
302+
| stats c = count(languages) by last_name
303+
| eval d = c + 1
304+
| project d, last_name
305+
""", "d", "last_name");
306+
}
307+
299308
public void testExplicitProject() {
300309
var plan = analyze("""
301310
from test

0 commit comments

Comments
 (0)