Skip to content

Commit fba36f6

Browse files
authored
New planner rule added to push predicates down in the query representation (#3324)
This introduces a new query planner rule that will be a part of a larger framework to canonicalize query representations. The rule pushes down predicates to ensure that they appear as low in the tree as possible. So, for example, suppose you had a query like: ```sql SELECT a, b, c FROM (SELECT a, b, c, d FROM T) WHERE d = ? ORDER BY a, b ``` This will produce a new query expression where the `d = ?` is pushed lower in the graph, something like: ```sql SELECT a, b, c FROM (SELECT a, b, c, d FROM T where d = ?) ORDER BY a, b ``` This simplification helps us during planning to push down predicate execution to the lowest part of the graph where they can go. Note that for the moment, this rule has _not_ been added to the default `PlanningRuleSet`. The reason for this is that some of the rewrite rules will have weird overlap with some of the other rules, and so we want to rewrite the query in one phase and then plan and optimize it in another. The framework for that is not there yet
1 parent 68f805a commit fba36f6

File tree

16 files changed

+2604
-136
lines changed

16 files changed

+2604
-136
lines changed

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

Lines changed: 81 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@
3333
import com.google.common.base.Preconditions;
3434
import com.google.common.base.Verify;
3535
import com.google.common.collect.ImmutableList;
36+
import com.google.common.collect.ImmutableSet;
3637
import com.google.common.collect.Iterables;
3738
import com.google.common.collect.Sets;
3839

3940
import javax.annotation.Nonnull;
4041
import java.util.Collection;
4142
import java.util.Collections;
4243
import java.util.Deque;
43-
import java.util.Objects;
44+
import java.util.List;
4445
import java.util.Optional;
4546
import java.util.Set;
4647
import java.util.function.Function;
@@ -288,22 +289,53 @@ public EvaluationContext getEvaluationContext() {
288289

289290
@Nonnull
290291
@Override
291-
@SuppressWarnings("PMD.CompareObjectsWithEquals")
292292
public Reference memoizeExploratoryExpression(@Nonnull final RelationalExpression expression) {
293-
if (expression.getQuantifiers().isEmpty()) {
294-
return memoizeLeafExpression(expression);
293+
return memoizeExploratoryExpressions(ImmutableSet.of(expression));
294+
}
295+
296+
@Nonnull
297+
@Override
298+
@SuppressWarnings("PMD.CompareObjectsWithEquals")
299+
public Reference memoizeExploratoryExpressions(@Nonnull final Collection<? extends RelationalExpression> expressions) {
300+
Preconditions.checkArgument(!expressions.isEmpty(), "Cannot create reference over empty expression collection");
301+
if (expressions.stream().allMatch(expression -> expression.getQuantifiers().isEmpty())) {
302+
return memoizeLeafExpressions(expressions);
295303
}
304+
//
305+
// This requires that for each expression in the expressions collection, all of its children have already been
306+
// placed in the memo structure. We then look to find a
307+
//
308+
// 1. Pick one variation.
309+
// 2. For that variation, find the set of references that point to each of its children. These form a set
310+
// of sets of candidate parents
311+
// 3. Take the set intersection of the candidates to get a new candidate set that is made up of all
312+
// expressions that point to _all_ children
313+
// 4. For each reference including an expression in the parent candidate set, look to see if that
314+
// candidate already contains all variations in its memo
315+
//
316+
// In other words, this uses the topology of the graph to find a candidate set of potential references.
317+
// Once that has been done, it checks if any of those already contain all variations. Note that we only
318+
// need to use one variation for that initial topology check. If we repeated steps 2-4 with the other
319+
// variations, then we will either re-tread existing candidate references (which must be missing at
320+
// least one variation) or it will be a new reference, but that reference must be missing at least
321+
// one child from the first variation and therefore cannot be reused
322+
//
296323
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.begin()));
297324
try {
298-
Preconditions.checkArgument(!(expression instanceof RecordQueryPlan));
325+
Preconditions.checkArgument(expressions.stream().noneMatch(expression -> expression instanceof RecordQueryPlan));
299326

300-
final var referencePathsList =
301-
expression.getQuantifiers()
302-
.stream()
327+
// Pick a candidate expression from the expressions collection. This will be used to do the
328+
final RelationalExpression expression = Iterables.getFirst(expressions, null);
329+
Verify.verify(expression != null, "should not get null from first element of non-empty expressions collection");
330+
331+
// For each child quantifier of this expression, look up the set of parent nodes in the
332+
// memo structure that point to the (already memoized) child
333+
final var referencePathsList = expression.getQuantifiers().stream()
303334
.map(Quantifier::getRangesOver)
304335
.map(traversal::getParentRefPaths)
305336
.collect(ImmutableList.toImmutableList());
306337

338+
// Gather the references to the expressions that include each child
307339
final var expressionToReferenceMap = new LinkedIdentityMap<RelationalExpression, Reference>();
308340
referencePathsList.stream()
309341
.flatMap(Collection::stream)
@@ -318,67 +350,73 @@ public Reference memoizeExploratoryExpression(@Nonnull final RelationalExpressio
318350
}
319351
});
320352

321-
final var referencingExpressions =
322-
referencePathsList.stream()
323-
.map(referencePaths ->
324-
referencePaths.stream()
325-
.map(Traversal.ReferencePath::getExpression)
326-
.collect(LinkedIdentitySet.toLinkedIdentitySet()))
327-
.collect(ImmutableList.toImmutableList());
353+
// From the traversal, get the sets of expressions that point to each child
354+
final List<Set<RelationalExpression>> referencingExpressions = referencePathsList.stream()
355+
.map(referencePaths ->
356+
referencePaths.stream()
357+
.map(Traversal.ReferencePath::getExpression)
358+
.collect(LinkedIdentitySet.toLinkedIdentitySet()))
359+
.collect(ImmutableList.toImmutableList());
328360

361+
// Compute the set of expressions that point to _all_ of the needed children
329362
final var referencingExpressionsIterator = referencingExpressions.iterator();
330363
final var commonReferencingExpressions = new LinkedIdentitySet<>(referencingExpressionsIterator.next());
331364
while (referencingExpressionsIterator.hasNext()) {
332365
commonReferencingExpressions.retainAll(referencingExpressionsIterator.next());
333366
}
334367

335-
commonReferencingExpressions.removeIf(commonReferencingExpression ->
336-
!Reference.isMemoizedExpression(expression, commonReferencingExpression));
337-
338-
if (!commonReferencingExpressions.isEmpty()) {
339-
Debugger.withDebugger(debugger ->
340-
debugger.onEvent(InsertIntoMemoEvent.reusedExpWithReferences(expression,
341-
commonReferencingExpressions.stream()
342-
.map(expressionToReferenceMap::get)
343-
.collect(ImmutableList.toImmutableList()))));
344-
final var reference =
345-
expressionToReferenceMap.get(Objects.requireNonNull(
346-
Iterables.getFirst(commonReferencingExpressions, null)));
347-
Verify.verifyNotNull(reference);
348-
Verify.verify(reference != this.root);
349-
return reference;
368+
// For each reference that has the right topology (that is, it points to all the children),
369+
// limit the candidates to those that actually contain all the variations in its memo
370+
final List<Reference> existingRefs = commonReferencingExpressions.stream()
371+
.map(expressionToReferenceMap::get)
372+
.filter(ref -> ref.containsAllInMemo(expressions, AliasMap.emptyMap(), false))
373+
.collect(ImmutableList.toImmutableList());
374+
375+
// If we found such a reference, re-use it
376+
if (!existingRefs.isEmpty()) {
377+
Reference existingReference = existingRefs.get(0);
378+
expressions.forEach(expr ->
379+
Debugger.withDebugger(debugger ->
380+
debugger.onEvent(InsertIntoMemoEvent.reusedExpWithReferences(expr, existingRefs))));
381+
Verify.verify(existingReference != this.root);
382+
return existingReference;
350383
}
351384

352-
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.newExp(expression)));
353-
354-
final var newRef = Reference.ofExploratoryExpression(plannerPhase.getTargetPlannerStage(), expression);
355-
traversal.addExpression(newRef, expression);
385+
// If we didn't find one, create a new reference and add it to the memo
386+
final var newRef = Reference.ofExploratoryExpressions(plannerPhase.getTargetPlannerStage(), expressions);
387+
expressions.forEach(expr -> {
388+
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.newExp(expr)));
389+
traversal.addExpression(newRef, expr);
390+
});
356391
return newRef;
357392
} finally {
358393
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.end()));
359394
}
360395
}
361396

362397
@Nonnull
363-
private Reference memoizeLeafExpression(@Nonnull final RelationalExpression expression) {
398+
private Reference memoizeLeafExpressions(@Nonnull final Collection<? extends RelationalExpression> expressions) {
364399
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.begin()));
365400
try {
366-
Preconditions.checkArgument(!(expression instanceof RecordQueryPlan));
367-
Preconditions.checkArgument(expression.getQuantifiers().isEmpty());
401+
Preconditions.checkArgument(expressions.stream()
402+
.allMatch(expression -> !(expression instanceof RecordQueryPlan) && expression.getQuantifiers().isEmpty()));
368403

369404
final var leafRefs = traversal.getLeafReferences();
370405

371406
for (final var leafRef : leafRefs) {
372-
for (final var member : leafRef.getExploratoryExpressions()) {
373-
if (Reference.isMemoizedExpression(expression, member)) {
407+
if (leafRef.containsAllInMemo(expressions, AliasMap.emptyMap(), false)) {
408+
for (RelationalExpression expression : expressions) {
374409
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.reusedExp(expression)));
375-
return leafRef;
376410
}
411+
return leafRef;
377412
}
378413
}
379-
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.newExp(expression)));
380-
final var newRef = Reference.ofExploratoryExpression(plannerPhase.getTargetPlannerStage(), expression);
381-
traversal.addExpression(newRef, expression);
414+
415+
final Reference newRef = Reference.ofExploratoryExpressions(plannerPhase.getTargetPlannerStage(), expressions);
416+
for (RelationalExpression expression : expressions) {
417+
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.newExp(expression)));
418+
traversal.addExpression(newRef, expression);
419+
}
382420
return newRef;
383421
} finally {
384422
Debugger.withDebugger(debugger -> debugger.onEvent(InsertIntoMemoEvent.end()));

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
2525

2626
import javax.annotation.Nonnull;
27+
import java.util.Collection;
2728

2829
/**
2930
* Interface to capture the parts of the memoizer API that only exploration rules are allowed to call.
@@ -34,10 +35,12 @@ public interface ExploratoryMemoizer {
3435
* semantically equivalent to the expression that is passed in, the reference containing the previously memoized
3536
* expression is returned allowing the planner to reuse that reference. If no such previously memoized expression is
3637
* found, a new reference is created and returned to the caller.
37-
* <br>
38+
*
39+
* <p>
3840
* The expression that is passed in must be an exploratory expression of the current planner phase. If this method
3941
* is used in an incompatible context and a reused reference is returned, the effects of mutating such a reference
4042
* through other planner logic are undefined and should be avoided.
43+
* </p>
4144
*
4245
* @param expression exploratory expression to memoize
4346
*
@@ -46,6 +49,23 @@ public interface ExploratoryMemoizer {
4649
@Nonnull
4750
Reference memoizeExploratoryExpression(@Nonnull RelationalExpression expression);
4851

52+
/**
53+
* Memoize the given collection of {@link RelationalExpression}s. If a reference of previously memoized expressions
54+
* is found that contains all of the given expressions, then this will return the existing reference. Otherwise,
55+
* this will add all of the expressions to the memoization structure and return a new reference.
56+
*
57+
* <p>
58+
* Like with {@link #memoizeExploratoryExpression(RelationalExpression)}, all of the passed in expressions must
59+
* be exploratory expressions of the current planner phase. See that method for details.
60+
* </p>
61+
*
62+
* @param expressions the collection of exploratory expressions to memoize
63+
* @return a new or reused reference
64+
* @see #memoizeExploratoryExpression(RelationalExpression)
65+
* */
66+
@Nonnull
67+
Reference memoizeExploratoryExpressions(@Nonnull Collection<? extends RelationalExpression> expressions);
68+
4969
/**
5070
* Return a new {@link ReferenceBuilder} for exploratory expressions. The expression passed in is memoized when
5171
* the builder's {@link ReferenceBuilder#reference()} is called. The expression is treated as an exploratory

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -421,14 +421,17 @@ boolean containsAllInMemo(@Nonnull final Reference otherRef,
421421
return true;
422422
}
423423

424-
for (final RelationalExpression otherExpression : otherRef.getExploratoryExpressions()) {
425-
if (!exploratoryMembers.containsInMemo(otherExpression, equivalenceMap)) {
426-
return false;
427-
}
428-
}
424+
return containsAllInMemo(otherRef.getExploratoryExpressions(), equivalenceMap, false)
425+
&& containsAllInMemo(otherRef.getFinalExpressions(), equivalenceMap, true);
426+
}
429427

430-
for (final RelationalExpression otherExpression : otherRef.getFinalExpressions()) {
431-
if (!finalMembers.containsInMemo(otherExpression, equivalenceMap)) {
428+
@API(API.Status.INTERNAL)
429+
boolean containsAllInMemo(@Nonnull final Collection<? extends RelationalExpression> expressions,
430+
@Nonnull final AliasMap equivalenceMap,
431+
boolean isFinal) {
432+
Members members = isFinal ? finalMembers : exploratoryMembers;
433+
for (final RelationalExpression otherExpression : expressions) {
434+
if (!members.containsInMemo(otherExpression, equivalenceMap)) {
432435
return false;
433436
}
434437
}
@@ -690,15 +693,6 @@ public String show(final boolean renderSingleGroups) {
690693
return PlannerGraphVisitor.show(renderSingleGroups, this);
691694
}
692695

693-
public static boolean isMemoizedExpression(@Nonnull final RelationalExpression expression,
694-
@Nonnull final RelationalExpression otherExpression) {
695-
if (!expression.getCorrelatedTo().equals(otherExpression.getCorrelatedTo())) {
696-
return false;
697-
}
698-
699-
return isMemoizedExpression(expression, otherExpression, AliasMap.emptyMap());
700-
}
701-
702696
@SuppressWarnings("PMD.CompareObjectsWithEquals")
703697
private static boolean isMemoizedExpression(@Nonnull final RelationalExpression member,
704698
@Nonnull final RelationalExpression otherExpression,
@@ -710,6 +704,14 @@ private static boolean isMemoizedExpression(@Nonnull final RelationalExpression
710704
return false;
711705
}
712706

707+
final Set<CorrelationIdentifier> originalCorrelatedTo = member.getCorrelatedTo();
708+
final Set<CorrelationIdentifier> translatedCorrelatedTo = equivalenceMap.definesOnlyIdentities()
709+
? originalCorrelatedTo
710+
: originalCorrelatedTo.stream().map(id -> equivalenceMap.getTargetOrDefault(id, id)).collect(ImmutableSet.toImmutableSet());
711+
if (!translatedCorrelatedTo.equals(otherExpression.getCorrelatedTo())) {
712+
return false;
713+
}
714+
713715
final List<? extends Quantifier> quantifiers = member.getQuantifiers();
714716
final List<? extends Quantifier> otherQuantifiers = otherExpression.getQuantifiers();
715717
if (member.getQuantifiers().size() != otherQuantifiers.size()) {

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
package com.apple.foundationdb.record.query.plan.cascades.matching.structure;
2222

2323
import com.apple.foundationdb.annotation.API;
24-
import com.apple.foundationdb.record.query.plan.cascades.Reference;
2524
import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
25+
import com.apple.foundationdb.record.query.plan.cascades.Reference;
2626
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
2727
import com.apple.foundationdb.record.query.plan.plans.RecordQueryPlan;
2828
import com.google.common.collect.ImmutableList;
@@ -129,17 +129,29 @@ public static BindingMatcher<Quantifier.ForEach> forEachQuantifierOverRef(@Nonnu
129129
}
130130

131131
@Nonnull
132-
public static BindingMatcher<Quantifier.ForEach> forEachQuantifierWithDefaultOnEmptyOverRef(@Nonnull final BindingMatcher<? extends Reference> downstream) {
132+
public static BindingMatcher<Quantifier.ForEach> forEachQuantifierOverRef(@Nonnull final BindingMatcher<? extends Reference> downstream, BindingMatcher<? super Boolean> defaultOnEmptyMatcher) {
133133
return typedWithDownstream(Quantifier.ForEach.class,
134134
Extractor.identity(),
135135
AllOfMatcher.matchingAllOf(Quantifier.ForEach.class,
136136
ImmutableList.of(
137137
typedWithDownstream(Quantifier.ForEach.class,
138138
Extractor.of(Quantifier.ForEach::isNullOnEmpty, name -> "withDefaultOnEmpty(" + name + ")"),
139-
PrimitiveMatchers.equalsObject(true)),
139+
defaultOnEmptyMatcher
140+
),
140141
typedWithDownstream(Quantifier.ForEach.class,
141142
Extractor.of(Quantifier::getRangesOver, name -> "rangesOver(" + name + ")"),
142-
downstream))));
143+
downstream)
144+
)));
145+
}
146+
147+
@Nonnull
148+
public static BindingMatcher<Quantifier.ForEach> forEachQuantifierWithDefaultOnEmptyOverRef(@Nonnull final BindingMatcher<? extends Reference> downstream) {
149+
return forEachQuantifierOverRef(downstream, PrimitiveMatchers.equalsObject(true));
150+
}
151+
152+
@Nonnull
153+
public static BindingMatcher<Quantifier.ForEach> forEachQuantifierWithoutDefaultOnEmptyOverRef(@Nonnull final BindingMatcher<? extends Reference> downstream) {
154+
return forEachQuantifierOverRef(downstream, PrimitiveMatchers.equalsObject(false));
143155
}
144156

145157
@Nonnull

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import com.apple.foundationdb.record.query.plan.plans.RecordQueryAbstractDataModificationPlan;
4141
import com.apple.foundationdb.record.query.plan.plans.RecordQueryAggregateIndexPlan;
4242
import com.apple.foundationdb.record.query.plan.plans.RecordQueryCoveringIndexPlan;
43+
import com.apple.foundationdb.record.query.plan.plans.RecordQueryDefaultOnEmptyPlan;
4344
import com.apple.foundationdb.record.query.plan.plans.RecordQueryDeletePlan;
4445
import com.apple.foundationdb.record.query.plan.plans.RecordQueryExplodePlan;
4546
import com.apple.foundationdb.record.query.plan.plans.RecordQueryFetchFromPartialRecordPlan;
@@ -163,6 +164,11 @@ public static SetMatcher<? extends RecordQueryPlan> exactlyPlansInAnyOrder(@Nonn
163164
return exactlyInAnyOrder(downstreams);
164165
}
165166

167+
@Nonnull
168+
public static BindingMatcher<RecordQueryDefaultOnEmptyPlan> defaultOnEmptyPlan(@Nonnull final BindingMatcher<? extends RecordQueryPlan> downstream) {
169+
return childrenPlans(RecordQueryDefaultOnEmptyPlan.class, all(downstream));
170+
}
171+
166172
@Nonnull
167173
public static BindingMatcher<RecordQueryFilterPlan> filter(@Nonnull final BindingMatcher<? extends Quantifier> downstream) {
168174
return ofTypeOwning(RecordQueryFilterPlan.class, any(downstream));

0 commit comments

Comments
 (0)