Skip to content

Commit 2e8fbe9

Browse files
authored
Esql skip null metrics in TS aggregations (#133087)
Resolves #129524 This is meant to add a rewrite rule to filter out null metrics. I'm opening this PR early to collect feedback from the Analytics Engine team on the approach. This rule scans the query plan to collect all of the metric attributes, creates an isNotNull expression for each, and then combines them into a single filter. For the initial version of this, we want to process any document that has any of the metrics in question, so we OR the filters together.
1 parent df8cedf commit 2e8fbe9

File tree

12 files changed

+355
-2
lines changed

12 files changed

+355
-2
lines changed

docs/changelog/133087.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 133087
2+
summary: Esql skip null metrics
3+
area: ES|QL
4+
type: enhancement
5+
issues:
6+
- 129524

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Attribute.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,11 @@ public static boolean dataTypeEquals(List<Attribute> left, List<Attribute> right
204204
*/
205205
public abstract boolean isDimension();
206206

207+
/**
208+
* @return true if the attribute represents a TSDB metric type
209+
*/
210+
public abstract boolean isMetric();
211+
207212
protected void checkAndSerializeQualifier(PlanStreamOutput out, TransportVersion version) throws IOException {
208213
if (version.supports(ESQL_QUALIFIERS_IN_ATTRIBUTES)) {
209214
out.writeOptionalCachedString(qualifier());

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/EmptyAttribute.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public boolean isDimension() {
5757
return false;
5858
}
5959

60+
@Override
61+
public boolean isMetric() {
62+
return false;
63+
}
64+
6065
@Override
6166
public boolean resolved() {
6267
return true;

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/FieldAttribute.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ public boolean isDimension() {
252252
return field.getTimeSeriesFieldType() == EsField.TimeSeriesFieldType.DIMENSION;
253253
}
254254

255+
@Override
256+
public boolean isMetric() {
257+
return field.getTimeSeriesFieldType() == EsField.TimeSeriesFieldType.METRIC;
258+
}
259+
255260
public EsField field() {
256261
return field;
257262
}

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/MetadataAttribute.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ protected String label() {
131131

132132
@Override
133133
public boolean isDimension() {
134+
// Metadata attributes cannot be dimensions. I think?
135+
return false;
136+
}
137+
138+
@Override
139+
public boolean isMetric() {
140+
// Metadata attributes definitely cannot be metrics.
134141
return false;
135142
}
136143

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/ReferenceAttribute.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,9 @@ protected String label() {
132132
public boolean isDimension() {
133133
return false;
134134
}
135+
136+
@Override
137+
public boolean isMetric() {
138+
return false;
139+
}
135140
}

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/UnresolvedAttribute.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,14 @@ protected String label() {
142142

143143
@Override
144144
public boolean isDimension() {
145-
return false;
145+
// We don't want to use this during the analysis phase, and this class does not exist after analysis
146+
throw new UnsupportedOperationException("This should never be called before the attribute is resolved");
147+
}
148+
149+
@Override
150+
public boolean isMetric() {
151+
// We don't want to use this during the analysis phase, and this class does not exist after analysis
152+
throw new UnsupportedOperationException("This should never be called before the attribute is resolved");
146153
}
147154

148155
@Override

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/index/EsIndex.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
public record EsIndex(
2323
String name,
24+
/** Map of field names to {@link EsField} instances representing that field */
2425
Map<String, EsField> mapping,
2526
Map<String, IndexMode> indexNameWithModes,
2627
/** Fields mapped only in some (but *not* all) indices. Since this is only used by the analyzer, it is not serialized. */

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PropagateEmptyRelation;
1313
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceStatsFilteredAggWithEval;
1414
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceStringCasingWithInsensitiveRegexMatch;
15+
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.IgnoreNullMetrics;
1516
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.InferIsNotNull;
1617
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.InferNonNullAggConstraint;
1718
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.LocalPropagateEmptyRelation;
@@ -44,6 +45,7 @@ public class LocalLogicalPlanOptimizer extends ParameterizedRuleExecutor<Logical
4445
new Batch<>(
4546
"Local rewrite",
4647
Limiter.ONCE,
48+
new IgnoreNullMetrics(),
4749
new ReplaceTopNWithLimitAndSort(),
4850
new ReplaceFieldWithConstantOrNull(),
4951
new InferIsNotNull(),
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.local;
9+
10+
import org.elasticsearch.index.IndexMode;
11+
import org.elasticsearch.xpack.esql.core.expression.Attribute;
12+
import org.elasticsearch.xpack.esql.core.expression.Expression;
13+
import org.elasticsearch.xpack.esql.expression.predicate.logical.Or;
14+
import org.elasticsearch.xpack.esql.expression.predicate.nulls.IsNotNull;
15+
import org.elasticsearch.xpack.esql.optimizer.rules.logical.OptimizerRules;
16+
import org.elasticsearch.xpack.esql.plan.logical.EsRelation;
17+
import org.elasticsearch.xpack.esql.plan.logical.Filter;
18+
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
19+
import org.elasticsearch.xpack.esql.plan.logical.TimeSeriesAggregate;
20+
import org.elasticsearch.xpack.esql.plan.logical.UnresolvedRelation;
21+
22+
import java.util.HashSet;
23+
import java.util.Set;
24+
25+
/**
26+
* <p><strong>NOTE: THIS RULE IS INTENDED TO BE RUN ONLY ONCE. DO NOT INCLUDE IT IN MULTI-RUN BATCHES.</strong> The expectation is that
27+
* this will be run as part of the {@link org.elasticsearch.xpack.esql.optimizer.LocalLogicalPlanOptimizer} in the local rewrite batch</p>
28+
* <p>
29+
* TSDB often ends up storing many null values for metrics columns (since not every time series contains every metric). However, loading
30+
* many null values can negatively impact query performance. To reduce that, this rule applies filters to remove null values on all
31+
* metrics involved in the query. In the case that there are multiple metrics, the not null checks are OR'd together, so we accept rows
32+
* where any of the metrics have values.
33+
* </p>
34+
*/
35+
public final class IgnoreNullMetrics extends OptimizerRules.OptimizerRule<TimeSeriesAggregate> {
36+
@Override
37+
public LogicalPlan rule(TimeSeriesAggregate agg) {
38+
Set<Attribute> metrics = new HashSet<>();
39+
agg.forEachExpression(Attribute.class, attr -> {
40+
if (attr.isMetric()) {
41+
metrics.add(attr);
42+
}
43+
});
44+
if (metrics.isEmpty()) {
45+
return agg;
46+
}
47+
Expression conditional = null;
48+
for (Attribute metric : metrics) {
49+
// Create an is not null check for each metric
50+
if (conditional == null) {
51+
conditional = new IsNotNull(agg.source(), metric);
52+
} else {
53+
// Join the is not null checks with OR nodes
54+
conditional = new Or(agg.source(), conditional, new IsNotNull(agg.source(), metric));
55+
}
56+
}
57+
return agg.replaceChild(new Filter(agg.source(), agg.child(), conditional));
58+
}
59+
60+
/**
61+
* Scans the given {@link LogicalPlan} to see if it is a "metrics mode" query
62+
*/
63+
private static boolean isMetricsQuery(LogicalPlan logicalPlan) {
64+
if (logicalPlan instanceof EsRelation r) {
65+
return r.indexMode() == IndexMode.TIME_SERIES;
66+
}
67+
if (logicalPlan instanceof UnresolvedRelation r) {
68+
return r.indexMode() == IndexMode.TIME_SERIES;
69+
}
70+
return false;
71+
}
72+
}

0 commit comments

Comments
 (0)