/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.sql.optimizer.rule.cost; import com.foundationdb.server.spatial.BoxLatLon; import com.foundationdb.sql.optimizer.rule.cost.CostEstimator.IndexIntersectionCoster; import com.foundationdb.sql.optimizer.rule.cost.CostEstimator.SelectivityConditions; import com.foundationdb.sql.optimizer.rule.range.RangeSegment; import static com.foundationdb.sql.optimizer.rule.OperatorAssembler.INSERTION_SORT_MAX_LIMIT; import static com.foundationdb.sql.optimizer.rule.cost.CostEstimator.simpleRound; import com.foundationdb.sql.optimizer.plan.*; import com.foundationdb.ais.model.Group; import com.foundationdb.ais.model.Table; import com.foundationdb.qp.rowtype.InternalIndexTypes; import com.foundationdb.server.error.AkibanInternalException; import com.foundationdb.server.types.common.BigDecimalWrapper; import com.geophile.z.Space; import com.geophile.z.SpatialObject; import java.math.BigDecimal; import java.util.*; public class PlanCostEstimator { protected CostEstimator costEstimator; private PlanEstimator planEstimator; public PlanCostEstimator(CostEstimator costEstimator) { this.costEstimator = costEstimator; } public CostEstimate getCostEstimate() { return costEstimator.adjustCostEstimate(planEstimator.getCostEstimate()); } public static final long NO_LIMIT = -1; public void setLimit(long limit) { planEstimator.setLimit(limit); } public void indexScan(IndexScan index) { planEstimator = new IndexScanEstimator(index); } public void spatialIndex(SingleIndexScan index) { planEstimator = new SpatialIndexEstimator(index); } public void flatten(TableGroupJoinTree tableGroup, TableSource indexTable, Set<TableSource> requiredTables) { planEstimator = new FlattenEstimator(planEstimator, tableGroup, indexTable, requiredTables); } public void groupScan(GroupScan scan, TableGroupJoinTree tableGroup, Set<TableSource> requiredTables) { planEstimator = new GroupScanEstimator(scan, tableGroup, requiredTables); } public void groupLoop(GroupLoopScan scan, TableGroupJoinTree tableGroup, Set<TableSource> requiredTables) { planEstimator = new GroupLoopEstimator(scan, tableGroup, requiredTables); } public void hKeyRow(ExpressionsHKeyScan scan) { planEstimator = new HKeyRowEstimator(scan); } public void fullTextScan(FullTextScan scan) { planEstimator = new FullTextScanEstimator(scan); } public void select(Collection<ConditionExpression> conditions, SelectivityConditions selectivityConditions) { planEstimator = new SelectEstimator(planEstimator, conditions, selectivityConditions); } public void sort(int nfields) { planEstimator = new SortEstimator(planEstimator, nfields); } protected abstract class PlanEstimator { protected PlanEstimator input; protected CostEstimate costEstimate = null; protected long limit = NO_LIMIT; protected PlanEstimator(PlanEstimator input) { this.input = input; } protected CostEstimate getCostEstimate() { if (costEstimate == null) { estimateCost(); } return costEstimate; } protected abstract void estimateCost(); protected void setLimit(long limit) { this.limit = limit; costEstimate = null; // Invalidate any previously computed cost. // NB: not passed to input; only set on end, which must then // propagate some limit back accordingly. } protected boolean hasLimit() { return (limit > 0); } protected CostEstimate inputCostEstimate() { return input.getCostEstimate(); } } protected class IndexScanEstimator extends PlanEstimator { IndexScan index; protected IndexScanEstimator(IndexScan index) { super(null); this.index = index; } @Override protected void estimateCost() { costEstimate = getScanOnlyCost(index); long totalCount = costEstimate.getRowCount(); if (hasLimit() && (limit < totalCount)) { if (index instanceof SingleIndexScan) { SingleIndexScan single = (SingleIndexScan)index; if (single.getConditionRange() == null) { costEstimate = costEstimator.costIndexScan(single.getIndex(), limit); return; } } // Multiple scans are involved; assume proportional. double setupCost = getScanSetupCost(index); double scanCost = costEstimate.getCost() - setupCost; costEstimate = new CostEstimate(limit, setupCost + scanCost * limit / totalCount); } } } protected class SpatialIndexEstimator extends PlanEstimator { SingleIndexScan index; protected SpatialIndexEstimator(SingleIndexScan index) { super(null); this.index = index; } @Override protected void estimateCost() { int nscans = 1; Space space = index.getIndex().space(); // TODO: Update for Geophile spatial join algorithm. costEstimate = costEstimator.costIndexScan(index.getIndex(), index.getEqualityComparands(), null, true, null, true); index.setScanCostEstimate(costEstimate); long totalRows = costEstimate.getRowCount(); long nrows = totalRows; if (hasLimit() && (limit < totalRows)) { nrows = limit; } if (nscans == 1) { if (nrows != totalRows) costEstimate = costEstimator.costIndexScan(index.getIndex(), nrows); return; } double setupCost = costEstimator.costIndexScan(index.getIndex(), 0).getCost(); double scanCost = costEstimate.getCost() - setupCost; costEstimate = new CostEstimate(limit, setupCost * nscans + scanCost * nrows / totalRows); } } protected static BigDecimal decimalConstant(ExpressionNode expr) { // Because the distance_lat_lon function returns a double, the radius // may be one for comparison. // Also numbers may accidentally be given as integers due to formatting. while (expr instanceof CastExpression) { expr = ((CastExpression)expr).getOperand(); } if (!(expr instanceof ConstantExpression)) return null; Object obj = ((ConstantExpression)expr).getValue(); if (obj instanceof BigDecimalWrapper) obj = ((BigDecimalWrapper)obj).asBigDecimal(); if (obj instanceof BigDecimal) return (BigDecimal)obj; else if (obj instanceof Number) return BigDecimal.valueOf((long)(((Number)obj).doubleValue() * 1.0e6), 6); else return null; } protected class FlattenEstimator extends PlanEstimator { private TableGroupJoinTree tableGroup; private TableSource indexTable; private Set<TableSource> requiredTables; protected FlattenEstimator(PlanEstimator input, TableGroupJoinTree tableGroup, TableSource indexTable, Set<TableSource> requiredTables) { super(input); this.tableGroup = tableGroup; this.indexTable = indexTable; this.requiredTables = requiredTables; } @Override protected void estimateCost() { CostEstimate flattenCost = costEstimator.costFlatten(tableGroup, indexTable, requiredTables); long flattenScale = flattenCost.getRowCount(); input.setLimit(hasLimit() ? // Ceiling number of inputs needed to get limit flattened. ((limit + flattenScale - 1) / flattenScale) : NO_LIMIT); costEstimate = inputCostEstimate().nest(flattenCost); } } protected class GroupScanEstimator extends PlanEstimator { private GroupScan scan; private TableGroupJoinTree tableGroup; private Set<TableSource> requiredTables; protected GroupScanEstimator(GroupScan scan, TableGroupJoinTree tableGroup, Set<TableSource> requiredTables) { super(null); this.scan = scan; this.tableGroup = tableGroup; this.requiredTables = requiredTables; } @Override protected void estimateCost() { if (hasLimit()) { Map<Table,Long> tableCounts = groupScanTableCountsToLimit(requiredTables, limit); if (tableCounts != null) { costEstimate = costEstimator.costPartialGroupScanAndFlatten(tableGroup, requiredTables, tableCounts); return; } } CostEstimate scanCost = costEstimator.costGroupScan(scan.getGroup().getGroup()); CostEstimate flattenCost = costEstimator.costFlattenGroup(tableGroup, requiredTables); costEstimate = scanCost.sequence(flattenCost); } } protected class GroupLoopEstimator extends PlanEstimator { private GroupLoopScan scan; private TableGroupJoinTree tableGroup; private Set<TableSource> requiredTables; protected GroupLoopEstimator(GroupLoopScan scan, TableGroupJoinTree tableGroup, Set<TableSource> requiredTables) { super(null); this.scan = scan; this.tableGroup = tableGroup; this.requiredTables = requiredTables; } @Override protected void estimateCost() { costEstimate = costEstimator.costFlattenNested(tableGroup, scan.getOutsideTable(), scan.getInsideTable(), scan.isInsideParent(), requiredTables); } } protected class HKeyRowEstimator extends PlanEstimator { private ExpressionsHKeyScan scan; protected HKeyRowEstimator(ExpressionsHKeyScan scan) { super(null); this.scan = scan; } @Override protected void estimateCost() { costEstimate = costEstimator.costHKeyRow(scan.getKeys()); } } protected class FullTextScanEstimator extends PlanEstimator { private FullTextScan scan; protected FullTextScanEstimator(FullTextScan scan) { super(null); this.scan = scan; } @Override protected void estimateCost() { costEstimate = new CostEstimate(Math.max(scan.getLimit(), 1), 1.0); } } protected class SelectEstimator extends PlanEstimator { private Collection<ConditionExpression> conditions; private SelectivityConditions selectivityConditions; protected SelectEstimator(PlanEstimator input, Collection<ConditionExpression> conditions, SelectivityConditions selectivityConditions) { super(input); this.conditions = conditions; this.selectivityConditions = selectivityConditions; } @Override protected void estimateCost() { double selectivity = costEstimator.conditionsSelectivity(selectivityConditions); // Need enough input rows before selection. input.setLimit(hasLimit() ? Math.round(limit / selectivity) : NO_LIMIT); CostEstimate inputCost = inputCostEstimate(); CostEstimate selectCost = costEstimator.costSelect(conditions, selectivity, inputCost.getRowCount()); costEstimate = inputCost.sequence(selectCost); } } protected class SortEstimator extends PlanEstimator { private int nfields; protected SortEstimator(PlanEstimator input, int nfields) { super(input); this.nfields = nfields; } @Override protected void estimateCost() { input.setLimit(NO_LIMIT); CostEstimate inputCost = inputCostEstimate(); CostEstimate sortCost; if (hasLimit() && (limit <= INSERTION_SORT_MAX_LIMIT)) { sortCost = costEstimator.costSortWithLimit(inputCost.getRowCount(), Math.min(limit, inputCost.getRowCount()), nfields); } else { sortCost = costEstimator.costSort(inputCost.getRowCount()); } costEstimate = inputCost.sequence(sortCost); } } protected CostEstimate getScanOnlyCost(IndexScan index) { CostEstimate result = index.getScanCostEstimate(); if (result == null) { if (index instanceof SingleIndexScan) { SingleIndexScan singleIndex = (SingleIndexScan) index; if (singleIndex.getConditionRange() == null) { result = costEstimator.costIndexScan(singleIndex.getIndex(), singleIndex.getEqualityComparands(), singleIndex.getLowComparand(), singleIndex.isLowInclusive(), singleIndex.getHighComparand(), singleIndex.isHighInclusive()); } else { CostEstimate cost = null; for (RangeSegment segment : singleIndex.getConditionRange().getSegments()) { CostEstimate acost = costEstimator.costIndexScan(singleIndex.getIndex(), singleIndex.getEqualityComparands(), segment.getStart().getValueExpression(), segment.getStart().isInclusive(), segment.getEnd().getValueExpression(), segment.getEnd().isInclusive()); if (cost == null) cost = acost; else cost = cost.union(acost); } if (cost == null) { cost = new CostEstimate(0, 0); // No segments means no real scan. } result = cost; } } else if (index instanceof MultiIndexIntersectScan) { MultiIndexIntersectScan multiIndex = (MultiIndexIntersectScan) index; result = costEstimator.costIndexIntersection(multiIndex, new IndexIntersectionCoster() { @Override public CostEstimate singleIndexScanCost(SingleIndexScan scan, CostEstimator costEstimator) { return getScanOnlyCost(scan); } }); } else { throw new AkibanInternalException("unknown index type: " + index + "(" + index.getClass() + ")"); } index.setScanCostEstimate(result); } return result; } protected double getScanSetupCost(IndexScan index) { if (index instanceof SingleIndexScan) { SingleIndexScan singleIndex = (SingleIndexScan)index; if (singleIndex.getConditionRange() == null) { return costEstimator.costIndexScan(singleIndex.getIndex(), 0).getCost(); } else { return costEstimator.costIndexScan(singleIndex.getIndex(), 0).getCost() * singleIndex.getConditionRange().getSegments().size(); } } else if (index instanceof MultiIndexIntersectScan) { MultiIndexIntersectScan multiIndex = (MultiIndexIntersectScan)index; return getScanSetupCost(multiIndex.getOutputIndexScan()) + getScanSetupCost(multiIndex.getSelectorIndexScan()); } else { return 0.0; } } protected Map<Table,Long> groupScanTableCountsToLimit(Set<TableSource> requiredTables, long limit) { // Find the required table with the highest ordinal; we'll need limit of those // rows and however many of the others come before it. // TODO: Not as good if multiple branches are being flattened; // fewer are needed to start, but repeats come in via branch // lookup. TableNode lastRequired = null; for (TableSource table : requiredTables) { if ((lastRequired == null) || (lastRequired.getOrdinal() < table.getTable().getOrdinal())) { lastRequired = table.getTable(); } } long childCount = costEstimator.getTableRowCount(lastRequired.getTable()); if (childCount <= limit) // Turns out we need the whole group before reaching the limit. return null; Map<Table,Long> tableCounts = new HashMap<>(); tableCounts.put(lastRequired.getTable(), limit); Table ancestor = lastRequired.getTable(); while (true) { ancestor = ancestor.getParentTable(); if (ancestor == null) break; long ancestorCount = costEstimator.getTableRowCount(ancestor); tableCounts.put(ancestor, // Ceiling number of ancestor needed to get limit of child. (limit * ancestorCount + (childCount - 1)) / childCount); } Group group = lastRequired.getTable().getGroup(); Map<Table,Long> moreCounts = new HashMap<>(); for (Table table : lastRequired.getTable().getAIS().getTables().values()) { if (table.getGroup() == group) { Table commonAncestor = table; while (!tableCounts.containsKey(commonAncestor)) { commonAncestor = commonAncestor.getParentTable(); } if (commonAncestor == table) continue; long ancestorCount = tableCounts.get(commonAncestor); if (table.getOrdinal() > lastRequired.getOrdinal()) // A table that isn't required; number skipped // depends on relative position. ancestorCount--; moreCounts.put(table, simpleRound(costEstimator.getTableRowCount(table) * ancestorCount, costEstimator.getTableRowCount(commonAncestor))); } } tableCounts.putAll(moreCounts); return tableCounts; } }