Skip to content

Commit

Permalink
Resolves #860: Try planning an IN predicate as an OR of equalities.
Browse files Browse the repository at this point in the history
If the planner detects that it can't implement an IN predicate using an
in-join (for example, because of an incompatible sort order), it tries
to rewrite it as an OR predicate instead.

This attempt is controlled by a configuration object, which also
includes the IndexScanPreference.
  • Loading branch information
nschiefer committed Mar 25, 2020
1 parent c749c38 commit 0546106
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 21 deletions.
4 changes: 2 additions & 2 deletions docs/ReleaseNotes.md
Expand Up @@ -45,12 +45,12 @@ The `FDBDatabase::getReadVersion()` method has been replaced with the `FDBRecord
* **Bug fix** Fix 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Bug fix** Fix 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Bug fix** Fix 5 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** Improvement 1 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** If an IN predicate cannot be planned using a nested loop join, we now attempt to plan it as an equivalent OR of equality predicates. [(Issue #860)](https://github.com/FoundationDB/fdb-record-layer/issues/860)
* **Performance** Improvement 2 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** Improvement 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** Improvement 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** Improvement 5 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Feature** Feature 1 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Feature** The `RecordQueryPlanner` now has a dedicated object for specifying configuration options. [(Issue #861)](https://github.com/FoundationDB/fdb-record-layer/pull/861)
* **Feature** Feature 2 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Feature** Feature 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Feature** Feature 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
Expand Down
Expand Up @@ -73,6 +73,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryUnorderedPrimaryKeyDistinctPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryUnorderedUnionPlan;
import com.apple.foundationdb.record.query.plan.temp.properties.FieldWithComparisonCountProperty;
import com.google.common.annotations.VisibleForTesting;

import javax.annotation.Nonnull;
Expand Down Expand Up @@ -116,7 +117,7 @@ public class RecordQueryPlanner implements QueryPlanner {

private boolean primaryKeyHasRecordTypePrefix;
@Nonnull
private IndexScanPreference indexScanPreference;
private RecordQueryPlannerConfiguration configuration;

public RecordQueryPlanner(@Nonnull RecordMetaData metaData, @Nonnull RecordStoreState recordStoreState) {
this(metaData, recordStoreState, null);
Expand Down Expand Up @@ -147,8 +148,11 @@ public RecordQueryPlanner(@Nonnull RecordMetaData metaData, @Nonnull RecordStore

primaryKeyHasRecordTypePrefix = metaData.primaryKeyHasRecordTypePrefix();
// If we are going to need type filters on Scan, index is safer without knowing any cardinalities.
indexScanPreference = metaData.getRecordTypes().size() > 1 && !primaryKeyHasRecordTypePrefix ?
IndexScanPreference.PREFER_INDEX : IndexScanPreference.PREFER_SCAN;
configuration = RecordQueryPlannerConfiguration.builder()
.setIndexScanPreference(metaData.getRecordTypes().size() > 1 && !primaryKeyHasRecordTypePrefix ?
IndexScanPreference.PREFER_INDEX : IndexScanPreference.PREFER_SCAN)
.setAttemptFailedInJoinAsOr(true)
.build();
}

/**
Expand All @@ -158,7 +162,7 @@ public RecordQueryPlanner(@Nonnull RecordMetaData metaData, @Nonnull RecordStore
*/
@Nonnull
public IndexScanPreference getIndexScanPreference() {
return indexScanPreference;
return configuration.getIndexScanPreference();
}

/**
Expand All @@ -167,11 +171,31 @@ public IndexScanPreference getIndexScanPreference() {
* Scanning without an index is more efficient, but will have to skip over unrelated record types.
* For that reason, it is safer to use an index, except when there is only one record type.
* If the meta-data has more than one record type but the record store does not, this can be overridden.
* If a {@link RecordQueryPlannerConfiguration} is already set using
* {@link #setConfiguration(RecordQueryPlannerConfiguration)} (RecordQueryPlannerConfiguration)} it will be retained,
* but the {@code IndexScanPreference} for the configuration will be replaced with the given preference.
* @param indexScanPreference whether to prefer index scan over record scan
*/
@Override
public void setIndexScanPreference(@Nonnull IndexScanPreference indexScanPreference) {
this.indexScanPreference = indexScanPreference;
configuration = this.configuration.asBuilder()
.setIndexScanPreference(indexScanPreference)
.build();
}

/**
* Set the {@link RecordQueryPlannerConfiguration} for this planner.
* If an {@link com.apple.foundationdb.record.query.plan.QueryPlanner.IndexScanPreference} is already set using
* {@link #setIndexScanPreference(IndexScanPreference)} then it will be ignored.
* @param configuration a configuration object for this planner
*/
public void setConfiguration(@Nonnull RecordQueryPlannerConfiguration configuration) {
this.configuration = configuration;
}

@Nonnull
public RecordQueryPlannerConfiguration getConfiguration() {
return configuration;
}

/**
Expand Down Expand Up @@ -295,7 +319,8 @@ private int compareIndexes(PlanContext planContext, @Nullable Index index1, @Nul

// Compatible behavior with older code: prefer an index on *just* the primary key.
private boolean preferIndexToScan(PlanContext planContext, @Nonnull Index index) {
switch (getIndexScanPreference()) {
IndexScanPreference indexScanPreference = getIndexScanPreference();
switch (indexScanPreference) {
case PREFER_INDEX:
return true;
case PREFER_SCAN:
Expand Down Expand Up @@ -341,12 +366,28 @@ private ScoredPlan planFilter(@Nonnull PlanContext planContext, @Nonnull QueryCo
@Nullable
private ScoredPlan planFilter(@Nonnull PlanContext planContext, @Nonnull QueryComponent filter, boolean needOrdering) {
final InExtractor inExtractor = new InExtractor(filter);
ScoredPlan withInAsOr = null;
if (planContext.query.getSort() != null) {
inExtractor.setSort(planContext.query.getSort(), planContext.query.isSortReverse());
if (!inExtractor.setSort(planContext.query.getSort(), planContext.query.isSortReverse()) && // needs to come first, to clear sort
getConfiguration().shouldAttemptFailedInJoinAsOr()) {
// Can't implement as an in join because of the sort order. Try as an OR instead.
withInAsOr = planFilter(planContext, inExtractor.asOr());
}
} else if (needOrdering) {
inExtractor.sortByClauses();
}
filter = inExtractor.subFilter();
final ScoredPlan withInJoin = planFilterWithInJoin(planContext, inExtractor, needOrdering);
if (withInAsOr != null) {
if (withInJoin == null || withInAsOr.score > withInJoin.score ||
FieldWithComparisonCountProperty.evaluate(withInAsOr.plan) < FieldWithComparisonCountProperty.evaluate(withInJoin.plan)) {
return withInAsOr;
}
}
return withInJoin;
}

private ScoredPlan planFilterWithInJoin(@Nonnull PlanContext planContext, @Nonnull InExtractor inExtractor, boolean needOrdering) {
final QueryComponent filter = inExtractor.subFilter();
planContext.rankComparisons = new RankComparisons(filter, planContext.indexes);
List<ScoredPlan> intersectionCandidates = new ArrayList<>();
ScoredPlan bestPlan = null;
Expand Down Expand Up @@ -1196,7 +1237,12 @@ private ScoredPlan planOrderedUnion(@Nonnull PlanContext planContext, @Nonnull L
if (unionPlan.getComplexity() > complexityThreshold) {
throw new RecordQueryPlanComplexityException(unionPlan);
}
return new ScoredPlan(1, unionPlan, Collections.emptyList(), anyDuplicates, includedRankComparisons);

// If we don't change this when shouldAttemptFailedInJoinAsOr() is true, then we _always_ pick the union plan,
// rather than the in join plan.
int score = getConfiguration().shouldAttemptFailedInJoinAsOr() ? 0 : 1;

return new ScoredPlan(score, unionPlan, Collections.emptyList(), anyDuplicates, includedRankComparisons);
}

@Nullable
Expand Down
@@ -0,0 +1,91 @@
/*
* RecordQueryPlannerConfiguration.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2020 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.apple.foundationdb.record.query.plan;

import com.apple.foundationdb.annotation.API;

import javax.annotation.Nonnull;

/**
* A set of configuration options for the {@link RecordQueryPlanner}.
*/
@API(API.Status.MAINTAINED)
public class RecordQueryPlannerConfiguration {
@Nonnull
private final QueryPlanner.IndexScanPreference indexScanPreference;
private boolean attemptFailedInJoinAsOr;

private RecordQueryPlannerConfiguration(@Nonnull QueryPlanner.IndexScanPreference indexScanPreference,
boolean attemptFailedInJoinAsOr) {
this.indexScanPreference = indexScanPreference;
this.attemptFailedInJoinAsOr = attemptFailedInJoinAsOr;
}

@Nonnull
public QueryPlanner.IndexScanPreference getIndexScanPreference() {
return indexScanPreference;
}

public boolean shouldAttemptFailedInJoinAsOr() {
return attemptFailedInJoinAsOr;
}

@Nonnull
public Builder asBuilder() {
return new Builder(this);
}

@Nonnull
public static Builder builder() {
return new Builder();
}

/**
* A builder for {@link RecordQueryPlannerConfiguration}.
*/
public static class Builder {
@Nonnull
private QueryPlanner.IndexScanPreference indexScanPreference = QueryPlanner.IndexScanPreference.PREFER_SCAN;
private boolean attemptFailedInJoinAsOr = false;

public Builder(@Nonnull RecordQueryPlannerConfiguration configuration) {
this.indexScanPreference = configuration.indexScanPreference;
this.attemptFailedInJoinAsOr = configuration.attemptFailedInJoinAsOr;
}

public Builder() {
}

public Builder setIndexScanPreference(@Nonnull QueryPlanner.IndexScanPreference indexScanPreference) {
this.indexScanPreference = indexScanPreference;
return this;
}

public Builder setAttemptFailedInJoinAsOr(boolean attemptFailedInJoinAsOr) {
this.attemptFailedInJoinAsOr = attemptFailedInJoinAsOr;
return this;
}

public RecordQueryPlannerConfiguration build() {
return new RecordQueryPlannerConfiguration(indexScanPreference, attemptFailedInJoinAsOr);
}
}
}
Expand Up @@ -46,6 +46,7 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

/**
Expand All @@ -61,13 +62,13 @@ public class InExtractor {
public InExtractor(QueryComponent filter) {
this.filter = filter;
inClauses = new ArrayList<>();
subFilter = extractInClauses(filter, new AtomicInteger(), Collections.emptyList());
subFilter = extractInClauses();
}

@SuppressWarnings("unchecked")
private QueryComponent extractInClauses(QueryComponent filter, AtomicInteger bindingIndex, @Nullable List<FieldKeyExpression> fields) {
if (filter instanceof ComponentWithComparison) {
final ComponentWithComparison withComparison = (ComponentWithComparison) filter;
private QueryComponent extractInClauses() {
final AtomicInteger bindingIndex = new AtomicInteger();
return mapClauses(filter, (withComparison, fields) -> {
if (withComparison.getComparison().getType() == Comparisons.Type.IN) {
String bindingName = Bindings.Internal.IN.bindingName(
withComparison.getName() + "__" + bindingIndex.getAndIncrement());
Expand All @@ -91,13 +92,48 @@ private QueryComponent extractInClauses(QueryComponent filter, AtomicInteger bin
}
return withComparison.withOtherComparison(new Comparisons.ParameterComparison(Comparisons.Type.EQUALS, bindingName, Bindings.Internal.IN));
} else {
return filter;
return withComparison;
}
}, Collections.emptyList());
}

@Nonnull
@SuppressWarnings("unchecked")
public QueryComponent asOr() {
return mapClauses(filter, (withComparison, fields) -> {
if (withComparison.getComparison().getType() == Comparisons.Type.IN) {
if (withComparison.getComparison() instanceof Comparisons.ParameterComparison) {
return withComparison;
} else {
final List<Object> comparands = (List<Object>) withComparison.getComparison().getComparand();
final List<QueryComponent> orBranches = new ArrayList<>();
for (Object comparand : comparands) {
orBranches.add(withComparison.withOtherComparison(new Comparisons.SimpleComparison(Comparisons.Type.EQUALS, comparand)));
}

// OR must have at least two branches.
if (orBranches.size() == 1) {
return orBranches.get(0);
} else {
return Query.or(orBranches);
}
}
} else {
return withComparison;
}

}, Collections.emptyList());
}

private QueryComponent mapClauses(QueryComponent filter, BiFunction<ComponentWithComparison, List<FieldKeyExpression>, QueryComponent> mapper, @Nullable List<FieldKeyExpression> fields) {
if (filter instanceof ComponentWithComparison) {
final ComponentWithComparison withComparison = (ComponentWithComparison) filter;
return mapper.apply(withComparison, fields);
} else if (filter instanceof ComponentWithChildren) {
ComponentWithChildren componentWithChildren = (ComponentWithChildren) filter;
return componentWithChildren.withOtherChildren(
componentWithChildren.getChildren().stream()
.map(component -> extractInClauses(component, bindingIndex, fields))
.map(component -> mapClauses(component, mapper, fields))
.collect(Collectors.toList())
);
} else if (filter instanceof ComponentWithSingleChild) {
Expand All @@ -108,7 +144,7 @@ private QueryComponent extractInClauses(QueryComponent filter, AtomicInteger bin
nestedFields.add(Key.Expressions.field(((NestedField) componentWithSingleChild).getFieldName()));
}
return componentWithSingleChild.withOtherChild(
extractInClauses(componentWithSingleChild.getChild(), bindingIndex, nestedFields));
mapClauses(componentWithSingleChild.getChild(), mapper, nestedFields));
} else if (filter instanceof ComponentWithNoChildren) {
return filter;
} else {
Expand All @@ -132,9 +168,9 @@ public QueryComponent subFilter() {
return subFilter;
}

public void setSort(@Nonnull KeyExpression key, boolean reverse) {
public boolean setSort(@Nonnull KeyExpression key, boolean reverse) {
if (inClauses.isEmpty()) {
return;
return true;
}
final List<KeyExpression> sortComponents = key.normalizeKeyForPositions();
int i = 0;
Expand All @@ -157,10 +193,11 @@ public void setSort(@Nonnull KeyExpression key, boolean reverse) {
if (!found) {
// There is a requested sort ahead of the ones from the IN's, so we can't do it.
cancel();
return;
return false;
}
i++;
}
return true;
}

public void sortByClauses() {
Expand Down

0 comments on commit 0546106

Please sign in to comment.