/* * ModeShape (http://www.modeshape.org) * * 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 org.modeshape.jcr.query.engine; import java.math.BigDecimal; import java.util.Collection; import java.util.Collections; import java.util.Map; import javax.jcr.query.qom.Constraint; import javax.jcr.query.qom.JoinCondition; import org.modeshape.common.annotation.Immutable; import org.modeshape.common.util.CheckArg; import org.modeshape.jcr.spi.index.provider.IndexProvider; @Immutable public final class IndexPlan implements Comparable<IndexPlan> { private static final Map<String, Object> NO_PARAMETERS = Collections.emptyMap(); private final String name; private final String workspaceName; private final String providerName; private final int costEstimate; private final long cardinalityEstimate; private final Float selectivityEstimate; private final Collection<Constraint> constraints; private final Collection<JoinCondition> joinConditions; private final Map<String, Object> parameters; public IndexPlan( String name, String workspaceName, String providerName, Collection<Constraint> constraints, Collection<JoinCondition> joinConditions, int costEstimate, long cardinalityEstimate, Float selectivityEstimate, Map<String, Object> parameters ) { CheckArg.isNotEmpty(name, "name"); CheckArg.isNonNegative(costEstimate, "costEstimate"); CheckArg.isNonNegative(cardinalityEstimate, "cardinalityEstimate"); this.name = name; this.workspaceName = workspaceName; this.providerName = providerName; // may be null or empty this.constraints = constraints != null ? constraints : Collections.<Constraint>emptyList(); this.joinConditions = joinConditions != null ? joinConditions : Collections.<JoinCondition>emptyList(); this.costEstimate = costEstimate; this.cardinalityEstimate = cardinalityEstimate; this.selectivityEstimate = (selectivityEstimate == null || selectivityEstimate < 0) ? null : selectivityEstimate; this.parameters = parameters == null ? NO_PARAMETERS : parameters; } /** * Return an estimate of the number of nodes that will be returned by this index given the constraints. For example, an index * that will return one node should have a cardinality of 1. * <p> * When possible, the actual cardinality should be used. However, since an accurate number is often expensive or impossible to * determine in the planning phase, the cardinality can instead represent a rough order of magnitude. A value of {@link Long#MAX_VALUE} * indicates that the cardinality is unknown. * </p> * <p> * Indexes with lower costs and lower {@link #getCardinalityEstimate() cardinalities} will be favored over other indexes. * </p> * * @return the cardinality estimate; never negative */ public long getCardinalityEstimate() { return cardinalityEstimate; } /** * Return an estimate of the cost of using the index for the query in question. An index that is expensive to use will have a * higher cost than another index that is less expensive to use. For example, if a {@link IndexProvider} that owns the index * is in a remote process, then the cost estimate will need to take into account the cost of transmitting the request with the * criteria and the response with all of the node that meet the criteria of the index. * <p> * Indexes with lower costs and lower {@link #getCardinalityEstimate() cardinalities} will be favored over other indexes. * </p> * * @return the cost estimate; never negative */ public int getCostEstimate() { return costEstimate; } /** * Determine if there is a {@link #getSelectivityEstimate() selectivity estimate}. This is equivalent to calling: * * <pre> * return getSelectivityEstimate() != null * </pre> * * @return true if there is an estimate of selectivity, or false if there is none */ public boolean hasSelectivityEstimate() { return selectivityEstimate != null; } /** * Get the estimate of the selectivity of this index for the query constraints. The selectivity is the ration of the number of * rows that will be returned to the total number of rows, or * * <pre> * selectivity = cardinality / total * </pre> * * Thus the selectivity (if known) will always be between 0 and 1.0, inclusive. * <p> * This method returns the estimated selectivity if it is know, or null if it is not known. * * @return the selectivity estimate, or null if there is none */ public Float getSelectivityEstimate() { return selectivityEstimate; } /** * Get the name of this index. * * @return the index name; never null */ public String getName() { return name; } /** * Get the name of the workspace to which this index applies. * * @return the workspace name; may be null if an implicit workspace is used */ public String getWorkspaceName() { return workspaceName; } /** * The name of the provider that owns the index. * * @return the provider name; null if the index is handled internally by ModeShape by something other than a provider */ public String getProviderName() { return providerName; } /** * Get the constraints that should be applied to this index if/when it is used. * * @return the constraints; may be null or empty if there are no constraints */ public Collection<Constraint> getConstraints() { return constraints; } /** * Get the join conditions that should be applied to this index if/when it is used. * * @return the join conditions; may be null or empty if there are no join conditions */ public Collection<JoinCondition> getJoinConditions() { return joinConditions; } /** * Get the provider-specific parameters for this index usage. * * @return the parameters; never null but possibly empty */ public Map<String, Object> getParameters() { return parameters; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getName()); sb.append(" cost=").append(getCostEstimate()); sb.append(", cardinality=").append(getCardinalityEstimate()); for (Map.Entry<String, Object> entry : parameters.entrySet()) { sb.append(", ").append(entry.getKey()).append("=").append(entry.getValue()); } return sb.toString(); } @Override public int compareTo( IndexPlan that ) { if (that == this) return 0; if (that == null) return 1; int thisCostEstimate = this.getCostEstimate(); long thisCardinalityEstimate = this.getCardinalityEstimate(); int thatCostEstimate = that.getCostEstimate(); long thatCardinalityEstimate = that.getCardinalityEstimate(); // first take care of the cases when the cardinality is unknown if (thisCardinalityEstimate == Long.MAX_VALUE && thatCardinalityEstimate == Long.MAX_VALUE) { // if both have unknown cardinality, we favor the one with the lowest cost and if those are equal as well, we // compare lexicographically the names return thisCostEstimate != thatCostEstimate ? Integer.compare(thisCostEstimate, thatCostEstimate) : this.name.compareTo(that.name); } else if (thatCardinalityEstimate == Long.MAX_VALUE) { // "that" index has unknown cardinality, so we always favor "this" which has a known cardinality return -1; } else if (thisCardinalityEstimate == Long.MAX_VALUE) { // "this" index has unknown cardinality, so we always favor "that" which has a known cardinality return 1; } if (thisCostEstimate == thatCostEstimate && thisCardinalityEstimate == thatCardinalityEstimate) { // in case both the costs and the cardinalities are the same, we compare lexicographically the names so that we're // consistently selecting the same index return this.name.compareTo(that.name); } // by default we always favor the lowest (cost * cardinality) value // this means that if 2 indexes have the same cost (i.e. are from the same provider) we'll use the one which gives us fewer nodes BigDecimal thisCostByCardinality = BigDecimal.valueOf(thisCostEstimate).multiply(BigDecimal.valueOf(thisCardinalityEstimate)); BigDecimal thatCostByCardinality = BigDecimal.valueOf(thatCostEstimate).multiply(BigDecimal.valueOf(thatCardinalityEstimate)); return thisCostByCardinality.compareTo(thatCostByCardinality); } }