Skip to content

Commit 29f2f1f

Browse files
authored
Scoring and costing query predicates (#3427)
This introduces a simple algorithm for scoring predicate complexity based on the predicate's tree diameter (defined as the longest path between any two leaf nodes in the predicate tree, assuming an undirected tree, as per Cormen et al.'s *Introduction to Algorithms*). To facilitate this, a `diameter()` method has been added to the `TreeLike` class for calculating the diameter of a `SelectExpression`'s predicates. Furthermore, a `PredicateComplexityProperty` is introduced, which returns the maximum predicate diameter found within a `RelationalExpression` tree. This property is then leveraged in the `RewritingCostModel` as a tie-breaker: when expressions have identical costs, the expression with the smallest maximum predicate diameter is preferred. This fixes #3419.
1 parent f603240 commit 29f2f1f

File tree

6 files changed

+313
-8
lines changed

6 files changed

+313
-8
lines changed

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ExpressionPropertiesMap.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
2424
import com.apple.foundationdb.record.query.plan.cascades.properties.ExpressionCountProperty;
25+
import com.apple.foundationdb.record.query.plan.cascades.properties.PredicateComplexityProperty;
2526
import com.apple.foundationdb.record.query.plan.plans.RecordQueryPlan;
2627
import com.google.common.base.Verify;
2728
import com.google.common.collect.ImmutableList;
@@ -282,7 +283,10 @@ public <P> Map<RecordQueryPlan, P> propertyValueForPlans(@Nonnull final Expressi
282283

283284
@Nonnull
284285
public static ExpressionPropertiesMap<RelationalExpression> defaultForRewritePhase() {
285-
return new ExpressionPropertiesMap<>(RelationalExpression.class, ImmutableSet.of(),
286-
ImmutableSet.of(ExpressionCountProperty.selectCount()), ImmutableList.of());
286+
return new ExpressionPropertiesMap<>(RelationalExpression.class,
287+
ImmutableSet.of(),
288+
ImmutableSet.of(ExpressionCountProperty.selectCount(), ExpressionCountProperty.tableFunctionCount(),
289+
PredicateComplexityProperty.predicateComplexity()),
290+
ImmutableList.of());
287291
}
288292
}

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModel.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import javax.annotation.Nonnull;
2929

30+
import static com.apple.foundationdb.record.query.plan.cascades.properties.PredicateComplexityProperty.predicateComplexity;
3031
import static com.apple.foundationdb.record.query.plan.cascades.properties.PredicateHeightProperty.predicateHeight;
3132
import static com.apple.foundationdb.record.query.plan.cascades.properties.ExpressionCountProperty.selectCount;
3233
import static com.apple.foundationdb.record.query.plan.cascades.properties.ExpressionCountProperty.tableFunctionCount;
@@ -79,6 +80,15 @@ public int compare(final RelationalExpression a, final RelationalExpression b) {
7980
return Integer.compare(aTableFunctions, bTableFunctions);
8081
}
8182

83+
//
84+
// Choose the expression with the simplest predicate.
85+
//
86+
int aPredicateComplexity = predicateComplexity().evaluate(a);
87+
int bPredicateComplexity = predicateComplexity().evaluate(b);
88+
if (aPredicateComplexity != bPredicateComplexity) {
89+
return Integer.compare(aPredicateComplexity, bPredicateComplexity);
90+
}
91+
8292
//
8393
// If expressions are indistinguishable from a cost perspective, select one by its semanticHash.
8494
//

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/TreeLike.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,43 @@ default T replace(@Nonnull final UnaryOperator<T> replacementOperator) {
317317
}
318318

319319
/**
320-
* returns the height of the tree.
320+
* returns the height of the tree rooted at {@code this} {@code TreeLike}.
321321
* @return the height of the tree.
322322
*/
323323
default int height() {
324-
return Streams.stream(getChildren()).mapToInt(TreeLike::height).max().orElse(0) + 1;
324+
if (Iterables.isEmpty(getChildren())) {
325+
return 0;
326+
}
327+
return 1 + Streams.stream(getChildren()).mapToInt(TreeLike::height).max().orElseThrow();
328+
}
329+
330+
/**
331+
* returns the diameter of the tree. i.e. the maximum path between two leaves in the tree.
332+
* @return the diameter of the tree.
333+
*/
334+
default int diameter() {
335+
if (Iterables.isEmpty(getChildren())) {
336+
return 0;
337+
}
338+
int maxChildDiameter = 0;
339+
int maxChildHeight = 0;
340+
int secondMaxChildHeight = 0;
341+
int connectingEdgesCount = Math.min(Iterables.size(getChildren()), 2);
342+
for (final var child : getChildren()) {
343+
int diameter = child.diameter();
344+
if (diameter > maxChildDiameter) {
345+
maxChildDiameter = diameter;
346+
}
347+
int height = child.height();
348+
if (height > maxChildHeight) {
349+
secondMaxChildHeight = maxChildHeight;
350+
maxChildHeight = height;
351+
} else if (height > secondMaxChildHeight) {
352+
secondMaxChildHeight = height;
353+
}
354+
}
355+
return Math.max(connectingEdgesCount + maxChildHeight + secondMaxChildHeight,
356+
maxChildDiameter);
325357
}
326358

327359
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* PredicateComplexityProperty.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.record.query.plan.cascades.properties;
22+
23+
import com.apple.foundationdb.record.query.plan.cascades.ExpressionProperty;
24+
import com.apple.foundationdb.record.query.plan.cascades.Reference;
25+
import com.apple.foundationdb.record.query.plan.cascades.TreeLike;
26+
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
27+
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionVisitorWithDefaults;
28+
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionWithPredicates;
29+
import com.apple.foundationdb.record.query.plan.cascades.predicates.QueryPredicate;
30+
import com.google.common.base.Verify;
31+
import com.google.common.collect.ImmutableList;
32+
import com.google.common.collect.Iterables;
33+
34+
import javax.annotation.Nonnull;
35+
import java.util.List;
36+
import java.util.Objects;
37+
import java.util.function.Function;
38+
39+
/**
40+
This property traverses a {@link RelationalExpression} to find the maximum diameter of any {@link QueryPredicate}
41+
associated with {@link RelationalExpressionWithPredicates} implementations.
42+
*/
43+
public class PredicateComplexityProperty implements ExpressionProperty<Integer> {
44+
@Nonnull
45+
private static final PredicateComplexityProperty PREDICATE_COMPLEXITY = newInstance();
46+
47+
private final boolean isTracked;
48+
private final Function<? super RelationalExpression, Integer> scoringFunction;
49+
50+
private PredicateComplexityProperty(@Nonnull final Function<? super RelationalExpression, Integer> scoringFunction,
51+
final boolean isTracked) {
52+
this.scoringFunction = scoringFunction;
53+
this.isTracked = isTracked;
54+
}
55+
56+
@Nonnull
57+
@Override
58+
public PredicateComplexityVisitor createVisitor() {
59+
return new PredicateComplexityVisitor(scoringFunction, this);
60+
}
61+
62+
public int evaluate(@Nonnull final Reference reference) {
63+
return evaluate(reference.get());
64+
}
65+
66+
public int evaluate(@Nonnull final RelationalExpression expression) {
67+
return Objects.requireNonNull(createVisitor().visit(expression));
68+
}
69+
70+
@Nonnull
71+
public static PredicateComplexityProperty predicateComplexity() {
72+
return PREDICATE_COMPLEXITY;
73+
}
74+
75+
public static class PredicateComplexityVisitor implements RelationalExpressionVisitorWithDefaults<Integer> {
76+
@Nonnull
77+
private final Function<? super RelationalExpression, Integer> filter;
78+
@Nonnull
79+
private final PredicateComplexityProperty property;
80+
81+
private PredicateComplexityVisitor(@Nonnull final Function<? super RelationalExpression, Integer> filter,
82+
@Nonnull final PredicateComplexityProperty property) {
83+
this.filter = filter;
84+
this.property = property;
85+
}
86+
87+
@Nonnull
88+
@Override
89+
public Integer visitDefault(@Nonnull final RelationalExpression expression) {
90+
final var nodeMax = filter.apply(expression);
91+
final var nodeChildrenMax = fromChildren(expression).stream().mapToInt(Integer::intValue).max().orElse(0);
92+
return Math.max(nodeMax, nodeChildrenMax);
93+
}
94+
95+
@Nonnull
96+
private List<Integer> fromChildren(@Nonnull final RelationalExpression expression) {
97+
return expression.getQuantifiers()
98+
.stream()
99+
.map(quantifier -> forReference(quantifier.getRangesOver()))
100+
.collect(ImmutableList.toImmutableList());
101+
}
102+
103+
private int forReference(@Nonnull final Reference reference) {
104+
final var finalExpressions = reference.getFinalExpressions();
105+
Verify.verify(finalExpressions.size() == 1);
106+
if (property.isTracked) {
107+
final var memberResults =
108+
reference.getPropertyForExpressions(property).values();
109+
return Iterables.getOnlyElement(memberResults);
110+
}
111+
return visit(Iterables.getOnlyElement(finalExpressions));
112+
}
113+
}
114+
115+
@Nonnull
116+
private static PredicateComplexityProperty newInstance() {
117+
return new PredicateComplexityProperty(
118+
expr -> expr instanceof RelationalExpressionWithPredicates
119+
? ((RelationalExpressionWithPredicates)expr)
120+
.getPredicates()
121+
.stream()
122+
.map(TreeLike::diameter)
123+
.max(Integer::compareTo)
124+
.orElse(0)
125+
: 0,
126+
true);
127+
}
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* TreeDiameterTest.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.record.query.plan.cascades.values;
22+
23+
import com.google.common.base.Verify;
24+
import com.google.common.collect.ImmutableList;
25+
import org.assertj.core.api.Assertions;
26+
import org.junit.jupiter.api.Test;
27+
28+
import javax.annotation.Nonnull;
29+
import java.util.Random;
30+
31+
public class TreeDiameterTest {
32+
33+
@Nonnull
34+
private static final Random random = new Random();
35+
36+
@Nonnull
37+
private static Value valueOfDepth(int depth) {
38+
if (depth == 0) {
39+
return LiteralValue.ofScalar(random.nextInt(1000));
40+
}
41+
int childrenCount = random.nextInt(5) + 1;
42+
int branchingNodeIndex = random.nextInt(childrenCount);
43+
final var childrenValuesBuilder = ImmutableList.<Value>builder();
44+
for (int i = 0; i < childrenCount; i++) {
45+
if (i == branchingNodeIndex) {
46+
childrenValuesBuilder.add(valueOfDepth(depth - 1));
47+
} else {
48+
childrenValuesBuilder.add(LiteralValue.ofScalar(random.nextInt(1000)));
49+
}
50+
}
51+
return RecordConstructorValue.ofUnnamed(childrenValuesBuilder.build());
52+
}
53+
54+
@Nonnull
55+
private static Value valueOfDiameter(int leftDepth, int rightDepth) {
56+
Verify.verify(leftDepth + rightDepth > 2);
57+
final var left = valueOfDepth(leftDepth);
58+
final var right = valueOfDepth(rightDepth);
59+
// add noise
60+
var randomChildrenCount = random.nextInt(5);
61+
final var childrenValuesBuilder = ImmutableList.<Value>builder();
62+
63+
// add noise
64+
if (leftDepth > 1 && rightDepth > 1) {
65+
for (int i = 0; i < randomChildrenCount; i++) {
66+
childrenValuesBuilder.add(LiteralValue.ofScalar(random.nextInt(1000)));
67+
}
68+
}
69+
70+
// add left child
71+
childrenValuesBuilder.add(left);
72+
73+
// add noise
74+
if (leftDepth > 1 && rightDepth > 1) {
75+
for (int i = 0; i < randomChildrenCount; i++) {
76+
childrenValuesBuilder.add(LiteralValue.ofScalar(random.nextInt(1000)));
77+
}
78+
}
79+
80+
// add right child
81+
childrenValuesBuilder.add(right);
82+
83+
// add noise
84+
if (leftDepth > 1 && rightDepth > 1) {
85+
for (int i = 0; i < randomChildrenCount; i++) {
86+
childrenValuesBuilder.add(LiteralValue.ofScalar(random.nextInt(1000)));
87+
}
88+
}
89+
90+
return RecordConstructorValue.ofUnnamed(childrenValuesBuilder.build());
91+
}
92+
93+
@Test
94+
void testDiameterOfOrphanTree() {
95+
final var expectedDiameter = 0;
96+
final var actualValue = LiteralValue.ofScalar(42);
97+
Assertions.assertThat(actualValue.diameter()).isEqualTo(expectedDiameter);
98+
}
99+
100+
@Test
101+
void testDiameterPassingThroughRoot() {
102+
final var expectedDiameter = 5 + 2;
103+
final var actualValue = valueOfDiameter(2, 3);
104+
Assertions.assertThat(actualValue.diameter()).isEqualTo(expectedDiameter);
105+
}
106+
107+
@Test
108+
void testDiameterNotPassingThroughRoot() {
109+
final var expectedDiameter = 3 + 5 + 2;
110+
final var childTree = valueOfDiameter(3, 5);
111+
final var singleChildValue = RecordConstructorValue.ofUnnamed(ImmutableList.of(childTree));
112+
Assertions.assertThat(singleChildValue.diameter()).isEqualTo(expectedDiameter);
113+
}
114+
115+
@Test
116+
void testLargestDiameterNotPassingThroughRoot() {
117+
final var smallChildTreeDiameter = 3 + 4 + 2;
118+
final var smallChildTree = valueOfDiameter(3, 4);
119+
final var largeChildTreeDiameter = 60 + 40 + 2;
120+
final var largeChildTree = valueOfDiameter(60, 40);
121+
final var singleChildValue = RecordConstructorValue.ofUnnamed(ImmutableList.of(smallChildTree, largeChildTree));
122+
Assertions.assertThat(singleChildValue.diameter()).isEqualTo(largeChildTreeDiameter);
123+
}
124+
125+
@Test
126+
void testLargestDiameterPassingThroughRoot() {
127+
final var smallChildTree = valueOfDiameter(3, 30);
128+
final var largeChildTree = valueOfDiameter(6, 40);
129+
final var parent = RecordConstructorValue.ofUnnamed(ImmutableList.of(smallChildTree, largeChildTree));
130+
Assertions.assertThat(parent.diameter()).isEqualTo(30 + 1 + 40 + 1 + 2);
131+
}
132+
}

fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/values/ValueHeightTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ public class ValueHeightTest {
3737

3838
@Nonnull
3939
private static Value valueOfDepth(int depth) {
40-
assert depth > 0;
41-
if (depth == 1) {
40+
if (depth == 0) {
4241
return LiteralValue.ofScalar(random.nextInt(1000));
4342
}
4443
int childrenCount = random.nextInt(5) + 1;
@@ -56,9 +55,9 @@ private static Value valueOfDepth(int depth) {
5655

5756
@Test
5857
void valueHeightIsCalculatedCorrectly() {
59-
Assertions.assertEquals(1, valueOfDepth(1).height());
58+
Assertions.assertEquals(0, valueOfDepth(0).height());
6059
for (int i = 0; i < 10000; i++) {
61-
final int depth = random.nextInt(100) + 1;
60+
final int depth = random.nextInt(100);
6261
Assertions.assertEquals(depth, valueOfDepth(depth).height());
6362
}
6463
}

0 commit comments

Comments
 (0)