/* * 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, ForeignIndexes> 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 * @param hints optional query hints * @throws IllegalArgumentException if filter is not supported */ public Result analyze(Filter filter, OrderingList ordering, QueryHints hints) 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> fullComparator = CompositeScore.fullComparator(hints); Collection> localIndexes = indexesFor(getStorableType()); if (localIndexes != null) { for (StorableIndex 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, hints); } 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> comp = CompositeScore.localForeignComparator(); if (comp.compare(bestForeignScore, bestLocalScore) < 0) { // Foreign is better. bestLocalScore = null; } else { // Local is better. bestForeignScore = null; } } CompositeScore bestScore; if (bestLocalScore != null) { bestScore = bestLocalScore; bestForeignIndex = null; bestForeignProperty = null; } else { bestScore = bestForeignScore; bestLocalIndex = null; } return new Result (filter, bestScore, bestLocalIndex, bestForeignIndex, bestForeignProperty, hints); } /** * @return null if no foreign indexes for property */ private synchronized ForeignIndexes 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; } if (chainedProp.isOuterJoin(0)) { // Outer joins cannot be optimized via foreign indexes. break evaluate; } int count = chainedProp.getChainCount(); for (int i=0; i> indexes = indexesFor(foreignType); foreignIndexes = new ForeignIndexes(chainedProp, indexes); } if (mForeignIndexCache == null) { mForeignIndexCache = Collections.synchronizedMap (new HashMap, ForeignIndexes>()); } 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 Collection> indexesFor(Class type) throws SupportException, RepositoryException { return mRepoAccess.storageAccessFor(type).getAllIndexes(); } public class Result { private final Filter mFilter; private final CompositeScore mScore; private final StorableIndex mLocalIndex; private final StorableIndex mForeignIndex; private final ChainedProperty mForeignProperty; private final QueryHints mHints; Result(Filter filter, CompositeScore score, StorableIndex localIndex, StorableIndex foreignIndex, ChainedProperty foreignProperty, QueryHints hints) { mFilter = filter; mScore = score; mLocalIndex = localIndex; mForeignIndex = foreignIndex; mForeignProperty = foreignProperty; mHints = hints; } /** * 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. */ public CompositeScore getCompositeScore() { return mScore; } /** * Remainder filter which overrides that in composite score. */ public Filter getRemainderFilter() { return mScore.getFilteringScore().getRemainderFilter(); } /** * Remainder orderings which override that in composite score. */ public OrderingList getRemainderOrdering() { return mScore.getOrderingScore().getRemainderOrdering(); } /** * 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); CompositeScore score = mScore .withRemainderFilter(remainderFilter) .withRemainderOrdering(remainderOrdering); return new Result(filter, score, mLocalIndex, mForeignIndex, mForeignProperty, mHints); } /** * 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 withRemainderFilter(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 withRemainderFilter(Filter remainderFilter) { Filter handledFilter = getCompositeScore().getFilteringScore().getHandledFilter(); Filter filter = andFilters(handledFilter, remainderFilter); CompositeScore score = mScore.withRemainderFilter(remainderFilter); return new Result(filter, score, mLocalIndex, mForeignIndex, mForeignProperty, mHints); } /** * Returns a new result with the remainder ordering replaced. */ public Result withRemainderOrdering(OrderingList remainderOrdering) { CompositeScore score = mScore.withRemainderOrdering(remainderOrdering); return new Result(mFilter, score, mLocalIndex, mForeignIndex, mForeignProperty, mHints); } /** * 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()); } } Filter remainderFilter = getRemainderFilter(); QueryExecutor executor; if (!handlesAnything()) { executor = new FullScanQueryExecutor(localAccess); } else if (localIndex == null) { // Use foreign executor. return JoinedQueryExecutor.build (mRepoAccess, getForeignProperty(), getFilter(), getOrdering(), mHints); } else { CompositeScore score = getCompositeScore(); FilteringScore fScore = score.getFilteringScore(); if (fScore.isKeyMatch()) { executor = new KeyQueryExecutor(localAccess, localIndex, fScore); } else { IndexedQueryExecutor ixExecutor = new IndexedQueryExecutor(localAccess, localIndex, score); executor = ixExecutor; if (ixExecutor.getCoveringFilter() != null) { remainderFilter = fScore.getCoveringRemainderFilter(); } } } 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; } @Override 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> mIndexes; // Cache of virtual indexes. private final Map, OrderedProperty[]> mVirtualIndexMap; /** * @param property type of property must be a joined Storable */ ForeignIndexes(ChainedProperty property, Collection> indexes) { mProperty = property; if (indexes == null || indexes.size() == 0) { mIndexes = Collections.emptyList(); } else { mIndexes = new ArrayList>(indexes); } mVirtualIndexMap = new HashMap, OrderedProperty[]>(); } /** * 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; } } }