/* * Copyright 2006 Amazon Technologies, Inc. or its affiliates. * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks * of Amazon Technologies, Inc. or its affiliates. All rights reserved. * * 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.amazon.carbonado.qe; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import com.amazon.carbonado.FetchException; import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.Storage; import com.amazon.carbonado.SupportException; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.PropertyFilter; import com.amazon.carbonado.filter.RelOp; import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.OrderedProperty; import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.info.StorableProperty; /** * Analyzes a simple query specification and determines which index is best * suited for its execution. Query filters passed to this analyzer cannot * contain any 'or' operations. * *
IndexedQueryAnalyzer is sharable and thread-safe. An instance for a
* particular Storable type can be cached, avoiding repeated construction
* cost. In addition, the analyzer caches learned foreign indexes.
*
* @author Brian S O'Neill
* @see UnionQueryAnalyzer
*/
public class IndexedQueryAnalyzer {
final Class mType;
final RepositoryAccess mRepoAccess;
// Growable cache which maps join properties to lists of usable foreign indexes.
private Map> mForeignIndexCache;
/**
* @param type type of storable being queried
* @param access repository access for examing available indexes
* @throws IllegalArgumentException if type or indexProvider is null
*/
public IndexedQueryAnalyzer(Class type, RepositoryAccess access) {
if (type == null || access == null) {
throw new IllegalArgumentException();
}
mType = type;
mRepoAccess = access;
}
public Class getStorableType() {
return mType;
}
/**
* @param filter optional filter which which must be {@link Filter#isBound
* bound} and cannot contain any logical 'or' operations.
* @param ordering optional properties which define desired ordering
* @throws IllegalArgumentException if filter is not supported
*/
public Result analyze(Filter filter, OrderingList ordering)
throws SupportException, RepositoryException
{
if (filter != null && !filter.isBound()) {
throw new IllegalArgumentException("Filter must be bound");
}
// First find best local index.
CompositeScore bestLocalScore = null;
StorableIndex bestLocalIndex = null;
final Comparator index : localIndexes) {
CompositeScore candidateScore =
CompositeScore.evaluate(index, filter, ordering);
if (bestLocalScore == null
|| fullComparator.compare(candidateScore, bestLocalScore) < 0)
{
bestLocalScore = candidateScore;
bestLocalIndex = index;
}
}
}
// Now try to find best foreign index.
if (bestLocalScore != null && bestLocalScore.getFilteringScore().isKeyMatch()) {
// Don't bother checking foreign indexes. The local one is perfect.
return new Result(filter, bestLocalScore, bestLocalIndex, null, null);
}
CompositeScore> bestForeignScore = null;
StorableIndex> bestForeignIndex = null;
ChainedProperty bestForeignProperty = null;
for (PropertyFilter propFilter : PropertyFilterList.get(filter)) {
ChainedProperty chainedProp = propFilter.getChainedProperty();
if (chainedProp.getChainCount() == 0) {
// Cannot possibly be a join, so move on.
continue;
}
ForeignIndexes foreignIndexes = getForeignIndexes(chainedProp);
if (foreignIndexes == null) {
continue;
}
for (StorableIndex> index : foreignIndexes.mIndexes) {
CompositeScore candidateScore = CompositeScore.evaluate
(foreignIndexes.getVirtualIndex(index),
index.isUnique(),
index.isClustered(),
filter,
ordering);
if (bestForeignScore == null
|| fullComparator.compare(candidateScore, bestForeignScore) < 0)
{
bestForeignScore = candidateScore;
bestForeignIndex = index;
bestForeignProperty = foreignIndexes.mProperty;
}
}
}
// Check if foreign index is better than local index.
if (bestLocalScore != null && bestForeignScore != null) {
// When comparing local index to foreign index, use a slightly less
// discriminating comparator, to prevent foreign indexes from
// looking too good.
Comparator getForeignIndexes(ChainedProperty chainedProp)
throws SupportException, RepositoryException
{
// Remove the last property as it is expected to be a simple storable
// property instead of a joined Storable.
chainedProp = chainedProp.trim();
ForeignIndexes foreignIndexes = null;
if (mForeignIndexCache != null) {
foreignIndexes = mForeignIndexCache.get(chainedProp);
if (foreignIndexes != null || mForeignIndexCache.containsKey(chainedProp)) {
return foreignIndexes;
}
}
// Check if property chain is properly joined and indexed along the way.
evaluate: {
if (!isProperJoin(chainedProp.getPrimeProperty())) {
break evaluate;
}
int count = chainedProp.getChainCount();
for (int i=0; i(chainedProp, indexes);
}
if (mForeignIndexCache == null) {
mForeignIndexCache = Collections.synchronizedMap
(new HashMap>());
}
mForeignIndexCache.put(chainedProp, foreignIndexes);
return foreignIndexes;
}
/**
* Checks if the property is a join and its internal properties are fully
* indexed.
*/
private boolean isProperJoin(StorableProperty> property)
throws SupportException, RepositoryException
{
if (!property.isJoin() || property.isQuery()) {
return false;
}
// Make up a filter over the join's internal properties and then search
// for an index that filters with no remainder.
Filter> filter = Filter.getOpenFilter(property.getEnclosingType());
int count = property.getJoinElementCount();
for (int i=0; i mFilter;
private final CompositeScore mScore;
private final StorableIndex mLocalIndex;
private final StorableIndex> mForeignIndex;
private final ChainedProperty mForeignProperty;
private final Filter mRemainderFilter;
private final OrderingList mRemainderOrdering;
Result(Filter filter,
CompositeScore score,
StorableIndex localIndex,
StorableIndex> foreignIndex,
ChainedProperty foreignProperty)
{
this(filter, score, localIndex, foreignIndex, foreignProperty,
score.getFilteringScore().getRemainderFilter(),
score.getOrderingScore().getRemainderOrdering());
}
private Result(Filter filter,
CompositeScore score,
StorableIndex localIndex,
StorableIndex> foreignIndex,
ChainedProperty foreignProperty,
Filter remainderFilter,
OrderingList remainderOrdering)
{
mFilter = filter;
mScore = score;
mLocalIndex = localIndex;
mForeignIndex = foreignIndex;
mForeignProperty = foreignProperty;
mRemainderFilter = remainderFilter;
mRemainderOrdering = remainderOrdering;
}
/**
* Returns true if the selected index does anything at all to filter
* results or to order them. If not, a filtered and sorted full scan
* makes more sense.
*/
public boolean handlesAnything() {
return mScore.getFilteringScore().hasAnyMatches() == true
|| mScore.getOrderingScore().getHandledCount() > 0;
}
/**
* Returns combined handled and remainder filter for this result.
*/
public Filter getFilter() {
return mFilter;
}
/**
* Returns combined handled and remainder orderings for this result.
*/
public OrderingList getOrdering() {
return mScore.getOrderingScore().getHandledOrdering().concat(getRemainderOrdering());
}
/**
* Returns the score on how well the selected index performs the
* desired filtering and ordering. When building a query executor, do
* not use the remainder filter and orderings available in the
* composite score. Instead, get them directly from this result object.
*/
public CompositeScore getCompositeScore() {
return mScore;
}
/**
* Remainder filter which overrides that in composite score.
*/
public Filter getRemainderFilter() {
return mRemainderFilter;
}
/**
* Remainder orderings which override that in composite score.
*/
public OrderingList getRemainderOrdering() {
return mRemainderOrdering;
}
/**
* Returns the local index that was selected, or null if a foreign
* index was selected.
*/
public StorableIndex getLocalIndex() {
return mLocalIndex;
}
/**
* Returns the foreign index that was selected, or null if a local
* index was selected. If a foreign index has been selected, then a
* {@link JoinedQueryExecutor} is needed.
*/
public StorableIndex> getForeignIndex() {
return mForeignIndex;
}
/**
* Returns the simple or chained property that maps to the selected
* foreign index. Returns null if foreign index was not selected. This
* property corresponds to the "targetToSourceProperty" of {@link
* JoinedQueryExecutor}.
*/
public ChainedProperty getForeignProperty() {
return mForeignProperty;
}
/**
* Returns true if local or foreign index is clustered. Scans of
* clustered indexes are generally faster.
*/
public boolean isIndexClustered() {
return (mLocalIndex != null && mLocalIndex.isClustered())
|| (mForeignIndex != null && mForeignIndex.isClustered());
}
/**
* Returns true if the given result uses the same index as this, and in
* the same way. The only allowed differences are in the remainder
* filter and orderings.
*/
public boolean canMergeRemainder(Result other) {
if (this == other || (!handlesAnything() && !other.handlesAnything())) {
return true;
}
if (equals(getLocalIndex(), other.getLocalIndex())
&& equals(getForeignIndex(), other.getForeignIndex())
&& equals(getForeignProperty(), other.getForeignProperty()))
{
return getCompositeScore().canMergeRemainder(other.getCompositeScore());
}
return false;
}
/**
* Merges the remainder filter and orderings of this result with the
* one given, returning a new result. Call canMergeRemainder first to
* verify if the merge makes any sense.
*/
public Result mergeRemainder(Result other) {
if (this == other) {
return this;
}
// Assuming canMergeRemainder returned true, each handled filter
// and the combined filter are all identical. This is just a safeguard.
Filter handledFilter =
orFilters(getCompositeScore().getFilteringScore().getHandledFilter(),
other.getCompositeScore().getFilteringScore().getHandledFilter());
Filter remainderFilter =
orFilters(getRemainderFilter(), other.getRemainderFilter());
OrderingList remainderOrdering =
getRemainderOrdering().concat(other.getRemainderOrdering()).reduce();
Filter filter = andFilters(handledFilter, remainderFilter);
return new Result
(filter, mScore, mLocalIndex, mForeignIndex, mForeignProperty,
remainderFilter, remainderOrdering);
}
/**
* Merges the remainder filter of this result with the given filter,
* returning a new result. If handlesAnything return true, then it
* doesn't usually make sense to call this method.
*/
public Result mergeRemainderFilter(Filter filter) {
return setRemainderFilter(orFilters(getRemainderFilter(), filter));
}
private Filter andFilters(Filter a, Filter b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
return a.and(b).reduce();
}
private Filter orFilters(Filter a, Filter b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
return a.or(b).reduce();
}
/**
* Returns a new result with the remainder filter replaced.
*/
public Result setRemainderFilter(Filter remainderFilter) {
return new Result
(mFilter, mScore, mLocalIndex, mForeignIndex, mForeignProperty,
remainderFilter, mRemainderOrdering);
}
/**
* Returns a new result with the remainder ordering replaced.
*/
public Result setRemainderOrdering(OrderingList remainderOrdering) {
return new Result
(mFilter, mScore, mLocalIndex, mForeignIndex, mForeignProperty,
mRemainderFilter, remainderOrdering);
}
/**
* Creates a QueryExecutor based on this result.
*/
public QueryExecutor createExecutor()
throws SupportException, FetchException, RepositoryException
{
StorableIndex localIndex = getLocalIndex();
StorageAccess localAccess = mRepoAccess.storageAccessFor(getStorableType());
if (localIndex != null) {
Storage delegate = localAccess.storageDelegate(localIndex);
if (delegate != null) {
return new DelegatedQueryExecutor(delegate, getFilter(), getOrdering());
}
}
QueryExecutor executor = baseLocalExecutor(localAccess);
if (executor == null) {
return JoinedQueryExecutor.build
(mRepoAccess, getForeignProperty(), getFilter(), getOrdering());
}
Filter remainderFilter = getRemainderFilter();
if (remainderFilter != null) {
executor = new FilteredQueryExecutor(executor, remainderFilter);
}
OrderingList remainderOrdering = getRemainderOrdering();
if (remainderOrdering.size() > 0) {
executor = new SortedQueryExecutor
(localAccess,
executor,
getCompositeScore().getOrderingScore().getHandledOrdering(),
remainderOrdering);
}
return executor;
}
/**
* Returns local executor or null if foreign executor should be used.
*/
private QueryExecutor baseLocalExecutor(StorageAccess localAccess) {
if (!handlesAnything()) {
return new FullScanQueryExecutor(localAccess);
}
StorableIndex localIndex = getLocalIndex();
if (localIndex != null) {
CompositeScore score = getCompositeScore();
FilteringScore fScore = score.getFilteringScore();
if (fScore.isKeyMatch()) {
return new KeyQueryExecutor(localAccess, localIndex, fScore);
}
return new IndexedQueryExecutor(localAccess, localIndex, score);
}
return null;
}
public String toString() {
return "IndexedQueryAnalyzer.Result {score="
+ getCompositeScore() + ", localIndex="
+ getLocalIndex() + ", foreignIndex="
+ getForeignIndex() + ", foreignProperty="
+ getForeignProperty() + ", remainderFilter="
+ getRemainderFilter() + ", remainderOrdering="
+ getRemainderOrdering() + '}';
}
private boolean equals(Object a, Object b) {
return a == null ? (b == null) : (a.equals(b));
}
}
private static class ForeignIndexes {
final ChainedProperty mProperty;
final List[]> mVirtualIndexMap;
/**
* @param property type of property must be a joined Storable
*/
ForeignIndexes(ChainedProperty property, Collection[]>();
}
/**
* Prepends local chained property with names of index elements,
* producing a virtual index on local storable. This allows
* CompositeScore to evaluate it.
*/
synchronized OrderedProperty[] getVirtualIndex(StorableIndex> index) {
OrderedProperty[] virtualProps = mVirtualIndexMap.get(index);
if (virtualProps != null) {
return virtualProps;
}
OrderedProperty>[] realProps = index.getOrderedProperties();
virtualProps = new OrderedProperty[realProps.length];
for (int i=realProps.length; --i>=0; ) {
OrderedProperty> realProp = realProps[i];
ChainedProperty virtualChained =
mProperty.append(realProp.getChainedProperty());
virtualProps[i] = OrderedProperty.get(virtualChained, realProp.getDirection());
}
mVirtualIndexMap.put(index, virtualProps);
return virtualProps;
}
}
}