blob: ae857e0be9d06e718f5f9fa99accd1f7e70438b3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.drill.exec.planner.index;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.calcite.plan.RelOptCost;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollationTraitDef;
import org.apache.calcite.rel.metadata.RelMdUtil;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexUtil;
import org.apache.drill.common.expression.LogicalExpression;
import org.apache.drill.exec.physical.base.DbGroupScan;
import org.apache.drill.exec.planner.common.DrillJoinRelBase;
import org.apache.drill.exec.planner.cost.DrillCostBase;
import org.apache.drill.exec.planner.cost.PluginCost;
import org.apache.drill.exec.planner.physical.PlannerSettings;
import org.apache.drill.exec.planner.physical.PrelUtil;
import org.apache.drill.exec.planner.physical.ScanPrel;
import org.apache.drill.exec.planner.common.DrillScanRelBase;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
public class IndexSelector {
static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(IndexSelector.class);
private static final double COVERING_TO_NONCOVERING_FACTOR = 100.0;
private RexNode indexCondition; // filter condition on indexed columns
private RexNode otherRemainderCondition; // remainder condition on all other columns
private double totalRows;
private Statistics stats; // a Statistics instance that will be used to get estimated rowcount for filter conditions
private IndexConditionInfo.Builder builder;
private List<IndexProperties> indexPropList;
private DrillScanRelBase primaryTableScan;
private IndexCallContext indexContext;
private RexBuilder rexBuilder;
public IndexSelector(RexNode indexCondition,
RexNode otherRemainderCondition,
IndexCallContext indexContext,
IndexCollection collection,
RexBuilder rexBuilder,
double totalRows) {
this.indexCondition = indexCondition;
this.otherRemainderCondition = otherRemainderCondition;
this.indexContext = indexContext;
this.totalRows = totalRows;
this.stats = indexContext.getGroupScan().getStatistics();
this.rexBuilder = rexBuilder;
this.builder =
IndexConditionInfo.newBuilder(indexCondition, collection, rexBuilder, indexContext.getScan());
this.primaryTableScan = indexContext.getScan();
this.indexPropList = Lists.newArrayList();
}
/**
* This constructor is to build selector for no index condition case (no filter)
* @param indexContext
*/
public IndexSelector(IndexCallContext indexContext) {
this.indexCondition = null;
this.otherRemainderCondition = null;
this.indexContext = indexContext;
this.totalRows = Statistics.ROWCOUNT_UNKNOWN;
this.stats = indexContext.getGroupScan().getStatistics();
this.rexBuilder = indexContext.getScan().getCluster().getRexBuilder();
this.builder = null;
this.primaryTableScan = indexContext.getScan();
this.indexPropList = Lists.newArrayList();
}
public void addIndex(IndexDescriptor indexDesc, boolean isCovering, int numProjectedFields) {
IndexProperties indexProps = new DrillIndexProperties(indexDesc, isCovering, otherRemainderCondition, rexBuilder,
numProjectedFields, totalRows, primaryTableScan);
indexPropList.add(indexProps);
}
/**
* This method analyzes an index's columns and starting from the first column, checks
* which part of the filter condition matches that column. This process continues with
* subsequent columns. The goal is to identify the portion of the filter condition that
* match the prefix columns. If there are additional conditions that don't match prefix
* columns, that condition is set as a remainder condition.
* @param indexProps
*/
public void analyzePrefixMatches(IndexProperties indexProps) {
RexNode initCondition = indexCondition.isAlwaysTrue() ? null : indexCondition;
Map<LogicalExpression, RexNode> leadingPrefixMap = Maps.newHashMap();
List<LogicalExpression> indexCols = indexProps.getIndexDesc().getIndexColumns();
boolean satisfiesCollation = false;
if (indexCols.size() > 0) {
if (initCondition != null) { // check filter condition
initCondition = IndexPlanUtils.getLeadingPrefixMap(leadingPrefixMap, indexCols, builder, indexCondition);
}
if (requiredCollation()) {
satisfiesCollation = buildAndCheckCollation(indexProps);
}
}
indexProps.setProperties(leadingPrefixMap, satisfiesCollation,
initCondition /* the remainder condition for indexed columns */, stats);
}
private boolean requiredCollation() {
return indexContext.getCollation() != null;
}
private boolean buildAndCheckCollation(IndexProperties indexProps) {
IndexDescriptor indexDesc = indexProps.getIndexDesc();
FunctionalIndexInfo functionInfo = indexDesc.getFunctionalInfo();
RelCollation inputCollation;
// for the purpose of collation we can assume that a covering index scan would provide
// the collation property that would be relevant for non-covering as well
ScanPrel indexScanPrel =
IndexPlanUtils.buildCoveringIndexScan(indexContext.getScan(), indexDesc.getIndexGroupScan(), indexContext, indexDesc);
inputCollation = indexScanPrel.getTraitSet().getTrait(RelCollationTraitDef.INSTANCE);
// we don't create collation for Filter because it will inherit the child's collation
if (indexContext.hasLowerProject()) {
inputCollation =
IndexPlanUtils.buildCollationProject(indexContext.getLowerProject().getProjects(), null,
indexContext.getScan(), functionInfo,indexContext);
}
if (indexContext.hasUpperProject()) {
inputCollation =
IndexPlanUtils.buildCollationProject(indexContext.getUpperProject().getProjects(), indexContext.getLowerProject(),
indexContext.getScan(), functionInfo, indexContext);
}
if ((inputCollation != null) &&
(inputCollation.satisfies(indexContext.getCollation()))) {
return true;
}
return false;
}
private void addIndexIntersections(List<IndexGroup> candidateIndexes,
IndexConditionInfo.Builder infoBuilder, long maxIndexesToIntersect) {
// Sort the non-covering indexes for candidate intersect indexes. Generating all
// possible combinations of candidates is not feasible so we guide our decision
// based on selectivity/collation considerations
IndexGroup indexesInCandidate = new IndexGroup();
final double SELECTIVITY_UNKNOWN = -1.0;
// We iterate over indexes upto planner.index.max_indexes_to_intersect. An index which allows
// filter pushdown is added to the existing list of indexes provided it reduces the selectivity
// by COVERING_TO_NONCOVERING_FACTOR
double prevSel = SELECTIVITY_UNKNOWN;
for (int idx = 0; idx < candidateIndexes.size(); idx++) {
// Maximum allowed indexes is intersect plan reached!
if (indexesInCandidate.numIndexes() == maxIndexesToIntersect) {
break;
}
// Skip covering indexes
if (candidateIndexes.get(idx).getIndexProps().get(0).isCovering()) {
continue;
}
// Check if adding the current index to the already existing intersect indexes is redundant
List<IndexDescriptor> candidateDescs = Lists.newArrayList();
for (IndexProperties prop : indexesInCandidate.getIndexProps()) {
candidateDescs.add(prop.getIndexDesc());
}
candidateDescs.add(candidateIndexes.get(idx).getIndexProps().get(0).getIndexDesc());
Map<IndexDescriptor, IndexConditionInfo> intersectIdxInfoMap
= infoBuilder.getIndexConditionMap(candidateDescs);
// Current index redundant(no conditions pushed down) - skip!
if (intersectIdxInfoMap.keySet().size() < candidateDescs.size()) {
continue;
}
IndexProperties curProp = candidateIndexes.get(idx).getIndexProps().get(0);
indexesInCandidate.addIndexProp(curProp);
double currSel = 1.0;
if (indexesInCandidate.isIntersectIndex()) {
for (IndexProperties prop : indexesInCandidate.getIndexProps()) {
currSel *= prop.getLeadingSelectivity();
}
if (prevSel == SELECTIVITY_UNKNOWN ||
currSel/prevSel < COVERING_TO_NONCOVERING_FACTOR) {
prevSel = currSel;
} else {
indexesInCandidate.removeIndexProp(curProp);
}
}
}
// If intersect plan was generated, add it to the set of candidate indexes.
if (indexesInCandidate.isIntersectIndex()) {
candidateIndexes.add(indexesInCandidate);
}
}
/**
* Run the index selection algorithm and return the top N indexes
*/
public void getCandidateIndexes(IndexConditionInfo.Builder infoBuilder, List<IndexGroup> coveringIndexes,
List<IndexGroup> nonCoveringIndexes, List<IndexGroup> intersectIndexes) {
RelOptPlanner planner = indexContext.getCall().getPlanner();
PlannerSettings settings = PrelUtil.getPlannerSettings(planner);
List<IndexGroup> candidateIndexes = Lists.newArrayList();
logger.info("index_plan_info: Analyzing {} indexes for prefix matches: {}",
indexPropList.size(), indexPropList);
// analysis phase
for (IndexProperties p : indexPropList) {
analyzePrefixMatches(p);
// only consider indexes that either have some leading prefix of the filter condition or
// can satisfy required collation
if (p.numLeadingFilters() > 0 || p.satisfiesCollation()) {
double selThreshold = p.isCovering() ? settings.getIndexCoveringSelThreshold() :
settings.getIndexNonCoveringSelThreshold();
// only consider indexes whose selectivity is <= the configured threshold OR consider
// all when full table scan is disable to avoid a CannotPlanException
if (settings.isDisableFullTableScan() || p.getLeadingSelectivity() <= selThreshold) {
IndexGroup index = new IndexGroup();
index.addIndexProp(p);
candidateIndexes.add(index);
}
else {
if (p.getLeadingSelectivity() > selThreshold) {
logger.debug("Skipping index {}. The leading selectivity {} is larger than threshold {}",
p.getIndexDesc().getIndexName(), p.getLeadingSelectivity(), selThreshold);
}
}
}
}
if (candidateIndexes.size() == 0) {
logger.info("index_plan_info: No suitable indexes found !");
return;
}
int max_candidate_indexes = (int)PrelUtil.getPlannerSettings(planner).getIndexMaxChosenIndexesPerTable();
// Ranking phase. Technically, we don't need to rank if there are fewer than max_candidate_indexes
// but we do it anyways for couple of reasons: the log output will show the indexes in a properly ranked
// order which helps diagnosing problems and secondly for internal unit/functional testing we want this code
// to be exercised even for few indexes
if (candidateIndexes.size() > 1) {
Collections.sort(candidateIndexes, new IndexComparator(planner, builder));
}
// Generate index intersections for ranking
addIndexIntersections(candidateIndexes, infoBuilder, settings.getMaxIndexesToIntersect());
// Sort again after intersect plan is added to the list
if (candidateIndexes.size() > 1) {
Collections.sort(candidateIndexes, new IndexComparator(planner, builder));
}
logger.info("index_plan_info: The top ranked indexes are: ");
int count = 0;
boolean foundCovering = false;
boolean foundCoveringCollation = false;
boolean foundNonCoveringCollation = false;
// pick the best N indexes
for (int i=0; i < candidateIndexes.size(); i++) {
IndexGroup index = candidateIndexes.get(i);
if (index.numIndexes() == 1
&& index.getIndexProps().get(0).isCovering()) {
IndexProperties indexProps = index.getIndexProps().get(0);
if (foundCoveringCollation) {
// if previously we already found a higher ranked covering index that satisfies collation,
// then skip this one (note that selectivity and cost considerations were already handled
// by the ranking phase)
logger.debug("index_plan_info: Skipping covering index {} because a higher ranked covering index with collation already exists.", indexProps.getIndexDesc().getIndexName());
continue;
}
coveringIndexes.add(index);
logger.info("index_plan_info: name: {}, covering, collation: {}, leadingSelectivity: {}, cost: {}",
indexProps.getIndexDesc().getIndexName(),
indexProps.satisfiesCollation(),
indexProps.getLeadingSelectivity(),
indexProps.getSelfCost(planner));
count++;
foundCovering = true;
if (indexProps.satisfiesCollation()) {
foundCoveringCollation = true;
}
} else if (index.numIndexes() == 1) { // non-covering
IndexProperties indexProps = index.getIndexProps().get(0);
// skip this non-covering index if (a) there was a higher ranked covering index
// with collation or (b) there was a higher ranked covering index and this
// non-covering index does not have collation
if (foundCoveringCollation ||
(foundCovering && !indexProps.satisfiesCollation())) {
logger.debug("index_plan_info: Skipping non-covering index {} because it does not have collation and a higher ranked covering index already exists.",
indexProps.getIndexDesc().getIndexName());
continue;
}
if (indexProps.satisfiesCollation()) {
foundNonCoveringCollation = true;
}
// all other non-covering indexes can be added to the list because 2 or more non-covering index could
// be considered for intersection later; currently the index selector is not costing the index intersection
nonCoveringIndexes.add(index);
logger.info("index_plan_info: name: {}, non-covering, collation: {}, leadingSelectivity: {}, cost: {}",
indexProps.getIndexDesc().getIndexName(),
indexProps.satisfiesCollation(),
indexProps.getLeadingSelectivity(),
indexProps.getSelfCost(planner));
count++;
} else { // intersect indexes
if (foundCoveringCollation ||
(foundCovering && !index.getIndexProps().get(index.numIndexes()-1).satisfiesCollation()) ||
foundNonCoveringCollation) {
continue;
}
IndexGroup intersectIndex = new IndexGroup();
double isectLeadingSel = 1.0;
String isectName = "Intersect-"+count;
for (IndexProperties indexProps : index.getIndexProps()) {
intersectIndex.addIndexProp(indexProps);
isectLeadingSel *= indexProps.getLeadingSelectivity();
logger.info("name: {}, {}, collation: {}, leadingSelectivity: {}, cost: {}",
indexProps.getIndexDesc().getIndexName(),
isectName,
indexProps.satisfiesCollation(),
indexProps.getLeadingSelectivity(),
indexProps.getSelfCost(planner));
}
logger.info("name: {}, intersect-idx, collation: {}, leadingSelectivity: {}, cost: {}",
isectName,
index.getIndexProps().get(index.numIndexes()-1).satisfiesCollation(),
isectLeadingSel,
index.getIndexProps().get(0).getIntersectCost(index, builder, planner));
intersectIndexes.add(intersectIndex);
}
if (count == max_candidate_indexes) {
break;
}
}
}
/**
* we assume all the indexes added in indexPropList are all applicable (and covering).
* For now this function is used and tested only in IndexScanWithSortOnlyPrule
*/
public IndexProperties getBestIndexNoFilter() {
if (indexPropList.size() == 0) {
return null;
}
RelOptPlanner planner = indexContext.getCall().getPlanner();
List<IndexGroup> candidateIndexes = Lists.newArrayList();
for (IndexProperties p : indexPropList) {
p.setSatisfiesCollation(buildAndCheckCollation(p));
IndexGroup index = new IndexGroup();
index.addIndexProp(p);
candidateIndexes.add(index);
}
Collections.sort(candidateIndexes, new IndexComparator(planner, builder));
return candidateIndexes.get(0).getIndexProps().get(0);
}
public static class IndexComparator implements Comparator<IndexGroup> {
private RelOptPlanner planner;
private IndexConditionInfo.Builder builder;
private PlannerSettings settings;
public IndexComparator(RelOptPlanner planner, IndexConditionInfo.Builder builder) {
this.planner = planner;
this.builder = builder;
this.settings = PrelUtil.getPlannerSettings(planner);
}
@Override
public int compare(IndexGroup index1, IndexGroup index2) {
// given a covering and a non-covering index, prefer covering index unless the
// difference in their selectivity is bigger than a configurable factor
List<IndexProperties> o1, o2;
boolean o1Covering, o2Covering, o1SatisfiesCollation, o2SatisfiesCollation;
int o1NumLeadingFilters, o2NumLeadingFilters;
double o1LeadingSelectivity, o2LeadingSelectivity;
DrillCostBase o1SelfCost, o2SelfCost;
o1 = index1.getIndexProps();
o2 = index2.getIndexProps();
Preconditions.checkArgument(o1.size() > 0 && o2.size() > 0);
if (o1.size() == 1) {
o1Covering = o1.get(0).isCovering();
o1NumLeadingFilters = o1.get(0).numLeadingFilters();
o1SatisfiesCollation = o1.get(0).satisfiesCollation();
o1LeadingSelectivity = o1.get(0).getLeadingSelectivity();
} else {
o1Covering = false;
// If the intersect plan is a left-deep join tree, the last index
// in the join would satisfy the collation. For now, assume no collation.
o1SatisfiesCollation = false;
o1NumLeadingFilters = o1.get(0).numLeadingFilters();
for (int idx=1; idx<o1.size(); idx++) {
o1NumLeadingFilters+=o1.get(idx).numLeadingFilters();
}
o1LeadingSelectivity = o1.get(0).getLeadingSelectivity();
for (int idx=1; idx<o1.size(); idx++) {
o1LeadingSelectivity*=o1.get(idx).getLeadingSelectivity();
}
}
if (o2.size() == 1) {
o2Covering = o2.get(0).isCovering();
o2NumLeadingFilters = o2.get(0).numLeadingFilters();
o2SatisfiesCollation = o2.get(0).satisfiesCollation();
o2LeadingSelectivity = o2.get(0).getLeadingSelectivity();
} else {
o2Covering = false;
// If the intersect plan is a left-deep join tree, the last index
// in the join would satisfy the collation. For now, assume no collation.
o2SatisfiesCollation = false;
o2NumLeadingFilters = o2.get(0).numLeadingFilters();
for (int idx=1; idx<o2.size(); idx++) {
o2NumLeadingFilters+=o2.get(idx).numLeadingFilters();
}
o2LeadingSelectivity = o2.get(0).getLeadingSelectivity();
for (int idx=1; idx<o2.size(); idx++) {
o2LeadingSelectivity*=o2.get(idx).getLeadingSelectivity();
}
}
if (o1Covering && !o2Covering) {
if (o1LeadingSelectivity/o2LeadingSelectivity < COVERING_TO_NONCOVERING_FACTOR) {
return -1; // covering is ranked higher (better) than non-covering
}
}
if (o2Covering && !o1Covering) {
if (o2LeadingSelectivity/o1LeadingSelectivity < COVERING_TO_NONCOVERING_FACTOR) {
return 1; // covering is ranked higher (better) than non-covering
}
}
if (!o1Covering && !o2Covering) {
if (o1.size() > 1) {
if (o1LeadingSelectivity/o2LeadingSelectivity < COVERING_TO_NONCOVERING_FACTOR) {
return -1; // Intersect is ranked higher than non-covering/intersect
}
} else if (o2.size() > 1) {
if (o2LeadingSelectivity/o1LeadingSelectivity < COVERING_TO_NONCOVERING_FACTOR) {
return -1; // Intersect is ranked higher than non-covering/intersect
}
}
}
if (o1SatisfiesCollation && !o2SatisfiesCollation) {
return -1; // index with collation is ranked higher (better) than one without collation
} else if (o2SatisfiesCollation && !o1SatisfiesCollation) {
return 1;
}
if (o1.size() == 1) {
o1SelfCost = (DrillCostBase) o1.get(0).getSelfCost(planner);
} else {
o1SelfCost = (DrillCostBase) o1.get(0).getIntersectCost(index1, builder, planner);
}
if (o2.size() == 1) {
o2SelfCost = (DrillCostBase) o2.get(0).getSelfCost(planner);
} else {
o2SelfCost = (DrillCostBase) o2.get(0).getIntersectCost(index2, builder, planner);
}
DrillCostBase cost1 = o1SelfCost;
DrillCostBase cost2 = o2SelfCost;
if (cost1.isLt(cost2)) {
return -1;
} else if (cost1.isEqWithEpsilon(cost2)) {
if (o1NumLeadingFilters > o2NumLeadingFilters) {
return -1;
} else if (o1NumLeadingFilters < o2NumLeadingFilters) {
return 1;
}
return 0;
} else {
return 1;
}
}
}
/**
* IndexProperties encapsulates the various metrics of a single index that are related to
* the current query. These metrics are subsequently used to rank the index in comparison
* with other indexes.
*/
public static class DrillIndexProperties implements IndexProperties {
private IndexDescriptor indexDescriptor; // index descriptor
private double leadingSel = 1.0; // selectivity of leading satisfiable conjunct
private double remainderSel = 1.0; // selectivity of all remainder satisfiable conjuncts
private boolean satisfiesCollation = false; // whether index satisfies collation
private boolean isCovering = false; // whether index is covering
private double avgRowSize; // avg row size in bytes of the selected part of index
private int numProjectedFields;
private double totalRows;
private DrillScanRelBase primaryTableScan = null;
private RelOptCost selfCost = null;
private List<RexNode> leadingFilters = Lists.newArrayList();
private Map<LogicalExpression, RexNode> leadingPrefixMap;
private RexNode indexColumnsRemainderFilter = null;
private RexNode otherColumnsRemainderFilter = null;
private RexBuilder rexBuilder;
public DrillIndexProperties(IndexDescriptor indexDescriptor,
boolean isCovering,
RexNode otherColumnsRemainderFilter,
RexBuilder rexBuilder,
int numProjectedFields,
double totalRows,
DrillScanRelBase primaryTableScan) {
this.indexDescriptor = indexDescriptor;
this.isCovering = isCovering;
this.otherColumnsRemainderFilter = otherColumnsRemainderFilter;
this.rexBuilder = rexBuilder;
this.numProjectedFields = numProjectedFields;
this.totalRows = totalRows;
this.primaryTableScan = primaryTableScan;
}
public void setProperties(Map<LogicalExpression, RexNode> prefixMap,
boolean satisfiesCollation,
RexNode indexColumnsRemainderFilter,
Statistics stats) {
this.indexColumnsRemainderFilter = indexColumnsRemainderFilter;
this.satisfiesCollation = satisfiesCollation;
leadingPrefixMap = prefixMap;
logger.info("index_plan_info: Index {}: leading prefix map: {}, satisfies collation: {}, remainder condition: {}",
indexDescriptor.getIndexName(), leadingPrefixMap, satisfiesCollation, indexColumnsRemainderFilter);
// iterate over the columns in the index descriptor and lookup from the leadingPrefixMap
// the corresponding conditions
leadingFilters = IndexPlanUtils.getLeadingFilters(leadingPrefixMap, indexDescriptor.getIndexColumns());
// compute the estimated row count by calling the statistics APIs
// NOTE: the calls to stats.getRowCount() below supply the primary table scan
// which is ok because its main use is to convert the ordinal-based filter
// to a string representation for stats lookup.
String idxIdentifier = stats.buildUniqueIndexIdentifier(this.getIndexDesc());
for (RexNode filter : leadingFilters) {
double filterRows = stats.getRowCount(filter, idxIdentifier, primaryTableScan /* see comment above */);
double sel = 1.0;
if (filterRows != Statistics.ROWCOUNT_UNKNOWN) {
sel = filterRows/totalRows;
logger.info("index_plan_info: Filter: {}, filterRows = {}, totalRows = {}, selectivity = {}",
filter, filterRows, totalRows, sel);
} else {
sel = RelMdUtil.guessSelectivity(filter);
if (stats.isStatsAvailable()) {
logger.debug("index_plan_info: Filter row count is UNKNOWN for filter: {}, using guess {}", filter, sel);
}
}
leadingSel *= sel;
}
logger.debug("index_plan_info: Combined selectivity of all leading filters: {}", leadingSel);
if (indexColumnsRemainderFilter != null) {
// The remainder filter is evaluated against the primary table i.e. NULL index
double remFilterRows = stats.getRowCount(indexColumnsRemainderFilter, null, primaryTableScan);
if (remFilterRows != Statistics.ROWCOUNT_UNKNOWN) {
remainderSel = remFilterRows/totalRows;
logger.debug("index_plan_info: Selectivity of index columns remainder filters: {}", remainderSel);
} else {
remainderSel = RelMdUtil.guessSelectivity(indexColumnsRemainderFilter);
if (stats.isStatsAvailable()) {
logger.debug("index_plan_info: Filter row count is UNKNOWN for remainder filter : {}, using guess {}",
indexColumnsRemainderFilter, remainderSel);
}
}
}
// get the average row size based on the leading column filter
avgRowSize = stats.getAvgRowSize(idxIdentifier, false);
if (avgRowSize == Statistics.AVG_ROWSIZE_UNKNOWN) {
avgRowSize = numProjectedFields * Statistics.AVG_COLUMN_SIZE;
if (stats.isStatsAvailable()) {
logger.debug("index_plan_info: Average row size is UNKNOWN based on leading filter: {}, using guess {}, columns {}, columnSize {}",
leadingFilters.size() > 0 ? leadingFilters.get(0).toString() : "<NULL>",
avgRowSize, numProjectedFields, Statistics.AVG_COLUMN_SIZE);
}
} else {
logger.debug("index_plan_info: Filter: {}, Average row size: {}",
leadingFilters.size() > 0 ? leadingFilters.get(0).toString() : "<NULL>", avgRowSize);
}
}
public double getLeadingSelectivity() {
return leadingSel;
}
public double getRemainderSelectivity() {
return remainderSel;
}
public boolean isCovering() {
return isCovering;
}
public double getTotalRows() {
return totalRows;
}
public IndexDescriptor getIndexDesc() {
return indexDescriptor;
}
public RexNode getLeadingColumnsFilter() {
return IndexPlanUtils.getLeadingColumnsFilter(leadingFilters, rexBuilder);
}
public RexNode getTotalRemainderFilter() {
return IndexPlanUtils.getTotalRemainderFilter(indexColumnsRemainderFilter, otherColumnsRemainderFilter, rexBuilder);
}
public boolean satisfiesCollation() {
return satisfiesCollation;
}
public void setSatisfiesCollation(boolean satisfiesCollation) {
this.satisfiesCollation = satisfiesCollation;
}
public RelOptCost getSelfCost(RelOptPlanner planner) {
if (selfCost != null) {
return selfCost;
}
selfCost = indexDescriptor.getCost(this, planner, numProjectedFields, IndexPlanUtils.getGroupScan(primaryTableScan));
return selfCost;
}
public RelOptCost getIntersectCost(IndexGroup index, IndexConditionInfo.Builder builder,
RelOptPlanner planner) {
return getIntersectCost(index, builder, planner, indexDescriptor.getPluginCostModel(), primaryTableScan);
}
public int numLeadingFilters() {
return leadingFilters.size();
}
public double getAvgRowSize() {
return avgRowSize;
}
public DrillScanRelBase getPrimaryTableScan() {
return this.primaryTableScan;
}
public RelOptCost getIntersectCost(IndexGroup index, IndexConditionInfo.Builder builder,
RelOptPlanner planner, PluginCost costBase, DrillScanRelBase scanRel) {
DrillCostBase.DrillCostFactory costFactory = (DrillCostBase.DrillCostFactory)planner.getCostFactory();
double totLeadRowCount = 1.0;
double totalRows = 0.0, totCpuCost = 0.0, totDiskCost = 0.0, totNetworkCost = 0.0, totMemoryCost = 0.0;
double rightSideRows = Statistics.ROWCOUNT_UNKNOWN;
DbGroupScan primaryTableGroupScan = (DbGroupScan) IndexPlanUtils.getGroupScan(scanRel);
RexNode remFilters;
final List<RexNode> remFilterList = Lists.newArrayList();
for (IndexProperties indexProps : index.getIndexProps()) {
remFilterList.add(indexProps.getTotalRemainderFilter());
}
remFilters = RexUtil.composeConjunction(scanRel.getCluster().getRexBuilder(), remFilterList, false);
for (IndexProperties indexProps : index.getIndexProps()) {
totalRows = indexProps.getTotalRows();
double leadRowCount = indexProps.getLeadingSelectivity() * indexProps.getRemainderSelectivity() * totalRows;
totLeadRowCount *= indexProps.getLeadingSelectivity();
double avgRowSize = indexProps.getAvgRowSize();
Preconditions.checkArgument(primaryTableGroupScan instanceof DbGroupScan);
// IO Cost (Filter evaluation CPU cost for pushed down filters)
double numBlocksIndex = Math.ceil((leadRowCount * avgRowSize) / costBase.getBlockSize(primaryTableGroupScan));
double diskCostIndex = numBlocksIndex * costBase.getSequentialBlockReadCost(primaryTableGroupScan);
totDiskCost += diskCostIndex;
// Join Cost (include network cost?) borrow from Hash Join
if (rightSideRows != Statistics.ROWCOUNT_UNKNOWN) {
DrillCostBase joinCost = (DrillCostBase) DrillJoinRelBase.computeHashJoinCostWithRowCntKeySize(planner,
rightSideRows, leadRowCount, 1);
totDiskCost += joinCost.getIo();
totCpuCost += joinCost.getCpu();
totMemoryCost += joinCost.getMemory();
// No NDV statistics; compute join rowcount as max of build side and probe side rows
rightSideRows = PrelUtil.getPlannerSettings(planner).getRowCountEstimateFactor() *
Math.max(leadRowCount, rightSideRows);
} else {
rightSideRows = leadRowCount;
}
remFilters = remainderCondition(indexProps.getIndexDesc(), builder, remFilters);
}
totLeadRowCount *= totalRows;
// for the primary table join-back each row may belong to a different block, so in general num_blocks = num_rows;
// however, num_blocks cannot exceed the total number of blocks of the table
DbGroupScan dbGroupScan = (DbGroupScan) primaryTableGroupScan;
double totalBlocksPrimary = Math.ceil((dbGroupScan.getColumns().size() *
costBase.getAverageColumnSize(dbGroupScan) * totalRows) / costBase.getBlockSize(dbGroupScan));
double diskBlocksPrimary = Math.min(totalBlocksPrimary, totLeadRowCount);
double diskCostPrimary = diskBlocksPrimary * costBase.getRandomBlockReadCost(dbGroupScan);
totDiskCost += diskCostPrimary;
// CPU cost of remainder condition evaluation over the selected rows
if (remFilters != null) {
totCpuCost += totLeadRowCount * DrillCostBase.COMPARE_CPU_COST;
}
double networkCost = 0.0; // TODO: add network cost once full table scan also considers network cost
return costFactory.makeCost(totLeadRowCount, totCpuCost, totDiskCost, totNetworkCost, totMemoryCost);
}
public RexNode remainderCondition(IndexDescriptor indexDesc, IndexConditionInfo.Builder builder,
RexNode initCondition) {
List<LogicalExpression> indexCols = indexDesc.getIndexColumns();
boolean prefix = true;
if (indexCols.size() > 0 && initCondition != null) {
int i=0;
while (prefix && i < indexCols.size()) {
LogicalExpression p = indexCols.get(i++);
List<LogicalExpression> prefixCol = ImmutableList.of(p);
IndexConditionInfo info = builder.indexConditionRelatedToFields(prefixCol, initCondition);
if(info != null && info.hasIndexCol) {
initCondition = info.remainderCondition;
if (initCondition.isAlwaysTrue()) {
// all filter conditions are accounted for, so if the remainder is TRUE, set it to NULL because
// we don't need to keep track of it for rest of the index selection
initCondition = null;
break;
}
} else {
prefix = false;
}
}
}
return initCondition;
}
}
}