/** * 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.join_enum; import com.foundationdb.server.error.CorruptedPlanException; import com.foundationdb.server.spatial.Spatial; import com.foundationdb.sql.optimizer.rule.EquivalenceFinder; import com.foundationdb.sql.optimizer.rule.JoinAndIndexPicker; import com.foundationdb.sql.optimizer.rule.PlanContext; import com.foundationdb.sql.optimizer.rule.SchemaRulesContext; import com.foundationdb.sql.optimizer.rule.cost.CostEstimator.SelectivityConditions; import com.foundationdb.sql.optimizer.rule.cost.PlanCostEstimator; import com.foundationdb.sql.optimizer.rule.join_enum.DPhyp.JoinOperator; import com.foundationdb.sql.optimizer.rule.range.ColumnRanges; import com.foundationdb.sql.optimizer.plan.*; import com.foundationdb.sql.optimizer.plan.Sort.OrderByExpression; import com.foundationdb.sql.optimizer.plan.TableGroupJoinTree.TableGroupJoinNode; import com.foundationdb.ais.model.*; import com.foundationdb.ais.model.Index.JoinType; import com.foundationdb.server.error.UnsupportedSQLException; import com.foundationdb.server.service.text.FullTextQueryBuilder; import com.foundationdb.server.types.TInstance; import com.foundationdb.server.types.common.funcs.GeoOverlaps; import com.foundationdb.server.types.texpressions.Comparison; import com.foundationdb.sql.types.DataTypeDescriptor; import com.foundationdb.sql.types.TypeId; import com.google.common.base.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** The goal of a indexes within a group. */ public class GroupIndexGoal implements Comparator<BaseScan> { private static final Logger logger = LoggerFactory.getLogger(GroupIndexGoal.class); static volatile Function<? super IndexScan,Void> intersectionEnumerationHook = null; // The overall goal. private QueryIndexGoal queryGoal; // The grouped tables. private TableGroupJoinTree tables; // All the conditions that might be indexable. private List<ConditionExpression> conditions; // Where they came from. private List<ConditionList> conditionSources; // The set of conditions that must be applied to this scan // (i.e. if the index doesn't cover them, a select will be required) // These are a subset of conditions private List<ConditionExpression> requiredConditions; // All the columns besides those in conditions that will be needed. private RequiredColumns requiredColumns; // Tables already bound outside. private Set<ColumnSource> boundTables; // Can an index be used to take care of sorting. // TODO: Make this a subset of queryGoal's sorting, based on what // tables are in what join, rather than an all or nothing. private boolean sortAllowed; // Mapping of Range-expressible conditions, by their column. lazy loaded. private Map<ColumnExpression,ColumnRanges> columnsToRanges; private PlanContext queryContext; public GroupIndexGoal(QueryIndexGoal queryGoal, TableGroupJoinTree tables, PlanContext queryContext) { this.queryGoal = queryGoal; this.tables = tables; this.queryContext = queryContext; if (queryGoal.getWhereConditions() != null) { conditionSources = Collections.singletonList(queryGoal.getWhereConditions()); conditions = queryGoal.getWhereConditions(); } else { conditionSources = Collections.emptyList(); conditions = Collections.emptyList(); } requiredConditions = conditions; requiredColumns = new RequiredColumns(tables); boundTables = queryGoal.getQuery().getOuterTables(); sortAllowed = true; } public QueryIndexGoal getQueryGoal() { return queryGoal; } public TableGroupJoinTree getTables() { return tables; } public Collection<? extends ColumnSource> getTableColumnSources() { List<TableSource> result = new ArrayList<>(); for (TableGroupJoinNode table : tables) { result.add(table.getTable()); } return result; } /** * @param boundTables Tables already bound by the outside. Columns of these tables * are like constants for the sake of this index selection. * @param queryJoins Joins that come from the query, or part of the query, * that an index is being searched for. * The type of these joins determines whether a column in a * condition is nullable. * Will generally match <code>joins</code>, except in the case * of a subquery with only one table group, where it is empty, * because it is the derived table that is nullable, not the inside * of the subquery. * @param joins Joins that apply to this part of the query. The conditions of these * are ones that might be indexed. * @param outsideJoins All joins for this query. A column used in one of these must * be supplied by its index source in determining whether that * is covering. * @param requiredJoins The joins that must be performed by the resulting join, * either in the index scan conditions or an outer * <code>Select</code> node, whose cost is then added. * @param sortAllowed <code>true</code> if sorting from this index selection might * accomplish sorting for the whole query. * @param extraConditions Conditions that can be indexed but were invented solely for * this index selection. For instance, a semi-join to * <code>VALUES</code> appears here as equality with a column. * @return Full list of all usable condition sources. If this index is chosen, its * conditions should each come, and be removed from, from one of these. */ public List<ConditionList> updateContext(Set<ColumnSource> boundTables, Collection<JoinOperator> queryJoins, Collection<JoinOperator> joins, Collection<JoinOperator> outsideJoins, Collection<JoinOperator> requiredJoins, boolean sortAllowed, ConditionList extraConditions) { setBoundTables(boundTables); this.sortAllowed = sortAllowed; setJoinConditions(queryJoins, joins, requiredJoins, extraConditions); updateRequiredColumns(joins, outsideJoins); return conditionSources; } public void setBoundTables(Set<ColumnSource> boundTables) { this.boundTables = boundTables; } private static boolean hasOuterJoin(Collection<JoinOperator> joins) { for (JoinOperator joinOp : joins) { switch (joinOp.getJoinType()) { case LEFT: case RIGHT: case FULL_OUTER: return true; } } return false; } public void setJoinConditions(Collection<JoinOperator> queryJoins, Collection<JoinOperator> joins, Collection<JoinOperator> requiredJoins, ConditionList extraConditions) { conditionSources = new ArrayList<>(); boolean hasOuterJoin = hasOuterJoin(queryJoins); if ((queryGoal.getWhereConditions() != null) && !hasOuterJoin) { conditionSources.add(queryGoal.getWhereConditions()); } for (JoinOperator join : joins) { ConditionList joinConditions = join.getJoinConditions(); if (joinConditions != null) conditionSources.add(joinConditions); } if (extraConditions != null) { conditionSources.add(extraConditions); } switch (conditionSources.size()) { case 0: conditions = Collections.emptyList(); break; case 1: conditions = conditionSources.get(0); break; default: conditions = new ArrayList<>(); for (ConditionList conditionSource : conditionSources) { conditions.addAll(conditionSource); } } buildRequiredConditions(requiredJoins, queryGoal.getWhereConditions(), hasOuterJoin); columnsToRanges = null; } private void buildRequiredConditions(Collection<JoinOperator> requiredJoins, ConditionList whereConditions, boolean hasOuterJoin) { requiredConditions = new ArrayList<>(); if (requiredJoins != null) { for (JoinOperator join : requiredJoins) { ConditionList joinConditions = join.getJoinConditions(); if (joinConditions != null) requiredConditions.addAll(joinConditions); } } if (whereConditions != null && !hasOuterJoin) { for (ConditionExpression condition : whereConditions) { requiredConditions.add(condition); } } } public void updateRequiredColumns(Collection<JoinOperator> joins, Collection<JoinOperator> outsideJoins) { requiredColumns.clear(); Collection<PlanNode> orderings = (queryGoal.getOrdering() == null) ? Collections.<PlanNode>emptyList() : Collections.<PlanNode>singletonList(queryGoal.getOrdering()); RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredColumns, orderings, conditions); queryGoal.getQuery().accept(filler); for (JoinOperator join : outsideJoins) { if (joins.contains(join)) continue; ConditionList joinConditions = join.getJoinConditions(); if (joinConditions != null) { for (ConditionExpression condition : joinConditions) { condition.accept(filler); } } } } /** Given a semi-join to a VALUES, see whether it can be turned * into a predicate on some column in this group, in which case it * can possibly be indexed. */ public InListCondition semiJoinToInList(ExpressionsSource values, Collection<JoinOperator> joins) { if (values.nFields() != 1) return null; ComparisonCondition ccond = null; boolean found = false; ConditionExpression joinCondition = onlyJoinCondition(joins); if (joinCondition instanceof ComparisonCondition) { ccond = (ComparisonCondition)joinCondition; if ((ccond.getOperation() == Comparison.EQ) && (ccond.getRight() instanceof ColumnExpression)) { ColumnExpression rcol = (ColumnExpression)ccond.getRight(); if ((rcol.getTable() == values) && (rcol.getPosition() == 0) && (ccond.getLeft() instanceof ColumnExpression)) { ColumnExpression lcol = (ColumnExpression)ccond.getLeft(); for (TableGroupJoinNode table : tables) { if (table.getTable() == lcol.getTable()) { found = true; break; } } } } } if (!found) return null; return semiJoinToInList(values, ccond, queryGoal.getRulesContext()); } protected static ConditionExpression onlyJoinCondition(Collection<JoinOperator> joins) { ConditionExpression result = null; for (JoinOperator join : joins) { if (join.getJoinConditions() != null) { for (ConditionExpression cond : join.getJoinConditions()) { if (result == null) result = cond; else return null; } } } return result; } public static InListCondition semiJoinToInList(ExpressionsSource values, ComparisonCondition ccond, SchemaRulesContext rulesContext) { List<ExpressionNode> expressions = new ArrayList<>(values.getExpressions().size()); for (List<ExpressionNode> row : values.getExpressions()) { expressions.add(row.get(0)); } DataTypeDescriptor sqlType = new DataTypeDescriptor(TypeId.BOOLEAN_ID, true); TInstance type = rulesContext.getTypesTranslator().typeForSQLType(sqlType); InListCondition cond = new InListCondition(ccond.getLeft(), expressions, sqlType, null, type); cond.setComparison(ccond); //cond.setPreptimeValue(new TPreptimeValue(AkBool.INSTANCE.instance(true))); return cond; } /** Populate given index usage according to goal. * @return <code>false</code> if the index is useless. */ public boolean usable(SingleIndexScan index) { setColumnsAndOrdering(index); int nequals = insertLeadingEqualities(index, conditions); if (index.getIndex().isSpatial()) return spatialUsable(index, nequals); List<ExpressionNode> indexExpressions = index.getColumns(); if (nequals < indexExpressions.size()) { ExpressionNode indexExpression = indexExpressions.get(nequals); if (indexExpression != null) { boolean foundInequalityCondition = false; for (ConditionExpression condition : conditions) { if (condition instanceof ComparisonCondition) { ComparisonCondition ccond = (ComparisonCondition)condition; if (ccond.getOperation() == Comparison.NE) continue; // ranges are better suited for != ExpressionNode otherComparand = matchingComparand(indexExpression, ccond); if (otherComparand != null) { Comparison op = ccond.getOperation(); if (otherComparand == ccond.getLeft()) op = ComparisonCondition.reverseComparison(op); index.addInequalityCondition(condition, op, otherComparand); foundInequalityCondition = true; } } } if (!foundInequalityCondition) { ColumnRanges range = rangeForIndex(indexExpression); if (range != null) index.addRangeCondition(range); } } } index.setOrderEffectiveness(determineOrderEffectiveness(index)); index.setCovering(determineCovering(index)); if ((index.getOrderEffectiveness() == IndexScan.OrderEffectiveness.NONE) && !index.hasConditions() && !index.isCovering()) return false; index.setCostEstimate(estimateCost(index)); return true; } private int insertLeadingEqualities(EqualityColumnsScan index, List<ConditionExpression> localConds) { int nequals = 0; List<ExpressionNode> indexExpressions = index.getColumns(); int ncols = indexExpressions.size(); while (nequals < ncols) { ExpressionNode indexExpression = indexExpressions.get(nequals); if (indexExpression == null) break; ConditionExpression equalityCondition = null; ExpressionNode otherComparand = null; for (ConditionExpression condition : localConds) { if (condition instanceof ComparisonCondition) { ComparisonCondition ccond = (ComparisonCondition)condition; if (ccond.getOperation() != Comparison.EQ) continue; // only doing equalities ExpressionNode comparand = matchingComparand(indexExpression, ccond); if (comparand != null) { equalityCondition = condition; otherComparand = comparand; break; } } else if (condition instanceof FunctionCondition) { FunctionCondition fcond = (FunctionCondition)condition; if (fcond.getFunction().equals("isNull") && (fcond.getOperands().size() == 1) && indexExpressionMatches(indexExpression, fcond.getOperands().get(0))) { ExpressionNode foperand = fcond.getOperands().get(0); equalityCondition = condition; otherComparand = new IsNullIndexKey(foperand.getSQLtype(), fcond.getSQLsource(), foperand.getType()); break; } } } if (equalityCondition == null) break; index.addEqualityCondition(equalityCondition, otherComparand); nequals++; } return nequals; } private ExpressionNode matchingComparand(ExpressionNode indexExpression, ComparisonCondition ccond) { ExpressionNode comparand; if (indexExpressionMatches(indexExpression, ccond.getLeft())) { comparand = ccond.getRight(); if (constantOrBound(comparand)) return comparand; } if (indexExpressionMatches(indexExpression, ccond.getRight())) { comparand = ccond.getLeft(); if (constantOrBound(comparand)) return comparand; } return null; } private static void setColumnsAndOrdering(SingleIndexScan index) { Index aisIndex = index.getIndex(); List<IndexColumn> indexColumns = aisIndex.getAllColumns(); int ncols = indexColumns.size(); int firstSpatialColumn, spatialColumns; SpecialIndexExpression.Function spatialFunction; Index.IndexMethod aisIndexMethod = aisIndex.getIndexMethod(); switch (aisIndexMethod) { case GEO_LAT_LON: case GEO_WKB: case GEO_WKT: firstSpatialColumn = aisIndex.firstSpatialArgument(); spatialColumns = aisIndex.spatialColumns(); if (spatialColumns == Spatial.LAT_LON_DIMENSIONS) { assert aisIndexMethod == Index.IndexMethod.GEO_LAT_LON : aisIndexMethod; spatialFunction = SpecialIndexExpression.Function.GEO_LAT_LON; } else if (spatialColumns == 1) { if (aisIndexMethod == Index.IndexMethod.GEO_WKB) { spatialFunction = SpecialIndexExpression.Function.GEO_WKB; } else if (aisIndexMethod == Index.IndexMethod.GEO_WKT) { spatialFunction = SpecialIndexExpression.Function.GEO_WKT; } else { spatialFunction = null; assert false : aisIndexMethod; } } else { spatialFunction = null; assert false : spatialColumns; } break; default: assert false : aisIndexMethod; /* and fall through */ case NORMAL: firstSpatialColumn = Integer.MAX_VALUE; spatialColumns = 0; spatialFunction = null; break; } List<OrderByExpression> orderBy = new ArrayList<>(ncols); List<ExpressionNode> indexExpressions = new ArrayList<>(ncols); int i = 0; while (i < ncols) { ExpressionNode indexExpression; boolean ascending; if (i == firstSpatialColumn) { List<ExpressionNode> operands = new ArrayList<>(spatialColumns); for (int j = 0; j < spatialColumns; j++) { operands.add(getIndexExpression(index, indexColumns.get(i++))); } indexExpression = new SpecialIndexExpression(spatialFunction, operands); ascending = true; } else { IndexColumn indexColumn = indexColumns.get(i++); indexExpression = getIndexExpression(index, indexColumn); ascending = indexColumn.isAscending(); } indexExpressions.add(indexExpression); orderBy.add(new OrderByExpression(indexExpression, ascending)); } index.setColumns(indexExpressions); index.setOrdering(orderBy); } // Take ordering from output index and adjust ordering of others to match. private static void installOrdering(IndexScan index, List<OrderByExpression> outputOrdering, int outputPeggedCount, int comparisonFields) { if (index instanceof SingleIndexScan) { List<OrderByExpression> indexOrdering = index.getOrdering(); if ((indexOrdering != null) && (indexOrdering != outputOrdering)) { // Order comparison fields the same way as output. // Try to avoid mixed mode: initial columns ordered // like first comparison, trailing columns ordered // like last comparison. int i = 0; boolean ascending = outputOrdering.get(outputPeggedCount).isAscending(); while (i < index.getPeggedCount()) { indexOrdering.get(i++).setAscending(ascending); } for (int j = 0; j < comparisonFields; j++) { assert outputOrdering.get(outputPeggedCount + j).isAscending() == ascending : "comparison ascending different order than initial"; indexOrdering.get(i++).setAscending(outputOrdering.get(outputPeggedCount + j).isAscending()); } boolean lastAscending = outputOrdering.get(outputPeggedCount + comparisonFields - 1).isAscending(); assert ascending == lastAscending : "lastAscending in different order than initial"; while (i < indexOrdering.size()) { indexOrdering.get(i++).setAscending(lastAscending); } } } else if (index instanceof MultiIndexIntersectScan) { MultiIndexIntersectScan multiIndex = (MultiIndexIntersectScan)index; installOrdering(multiIndex.getOutputIndexScan(), outputOrdering, outputPeggedCount, comparisonFields); installOrdering(multiIndex.getSelectorIndexScan(), outputOrdering, outputPeggedCount, comparisonFields); } } private ExpressionNode targetExpression(OrderByExpression targetColumn) { ExpressionNode targetExpression = targetColumn.getExpression(); if (targetExpression.isColumn()) { ColumnExpression column = (ColumnExpression)targetExpression; ColumnSource table = column.getTable(); if (table == queryGoal.getGrouping()) { targetExpression = queryGoal.getGrouping() .getField(column.getPosition()); } else if (table instanceof Project) { // Cf. ASTStatementLoader.sortsForDistinct(). Project project = (Project)table; if ((project.getOutput() == queryGoal.getOrdering()) && (queryGoal.getOrdering().getOutput() instanceof Distinct)) { targetExpression = project.getFields() .get(column.getPosition()); } } } return targetExpression; } // Determine how well this index does against the target. // Also, correct traversal order to match sort if possible. protected IndexScan.OrderEffectiveness determineOrderEffectiveness(SingleIndexScan index) { IndexScan.OrderEffectiveness result = IndexScan.OrderEffectiveness.NONE; if (!sortAllowed) return result; List<OrderByExpression> indexOrdering = index.getOrdering(); if (indexOrdering == null) return result; int nequals = index.getNEquality(); int nunions = index.getNUnions(); List<ExpressionNode> equalityColumns = null; if (nequals - nunions > 0) { equalityColumns = index.getColumns().subList(0, nequals - nunions); } try_sorted: if (queryGoal.getOrdering() != null) { int idx = nequals-nunions; for (OrderByExpression targetColumn : queryGoal.getOrdering().getOrderBy()) { // Get the expression by which this is ordering, recognizing the // special cases where the Sort is fed by GROUP BY or feeds DISTINCT. ExpressionNode targetExpression = targetExpression(targetColumn); OrderByExpression indexColumn = getIndexColumn(indexOrdering, idx); if (indexColumn == null && idx < nequals) { throw new CorruptedPlanException("No index column expression for union comparison"); } if (indexColumn != null) { boolean matchingColumn = orderingExpressionMatches(indexColumn, targetExpression); if (!matchingColumn && idx < nequals) { // if we we're trying the union column, but that failed, try just treating it as equals idx++; indexColumn = getIndexColumn(indexOrdering, idx); if (indexColumn != null) { matchingColumn = orderingExpressionMatches(indexColumn, targetExpression); if (matchingColumn) { index.setIncludeUnionAsEquality(true); } } } if (matchingColumn) { if (idx < nequals) { index.setIncludeUnionAsEquality(false); } if (idx >= index.getNKeyColumns()) index.setUsesAllColumns(true); idx++; continue; } } if (equalityColumns != null) { // Another possibility is that target ordering is // in fact unchanged due to equality condition. // TODO: Should this have been noticed earlier on // so that it can be taken out of the sort? if (equalityColumns.contains(targetExpression)) continue; } break try_sorted; } // Don't allow mixed order index lookups, just use the order of the first column boolean isAscending = queryGoal.getOrdering().getOrderBy().get(0).isAscending(); for (OrderByExpression indexColumn : indexOrdering) { if (indexColumn.isAscending() != isAscending) { indexColumn.setAscending(isAscending); } } boolean orderByOrdering = true; for (OrderByExpression targetColumn : queryGoal.getOrdering().getOrderBy()) { if (targetColumn.isAscending() != isAscending) { orderByOrdering = false; } } // If the order by ordering columns all matches the // order of the index (ASC or DESC), the index is sorted. if (orderByOrdering) { result = IndexScan.OrderEffectiveness.SORTED; } } if (queryGoal.getGrouping() != null) { boolean anyFound = false, allFound = true; List<ExpressionNode> groupBy = queryGoal.getGrouping().getGroupBy(); for (ExpressionNode targetExpression : groupBy) { int found = -1; for (int i = nequals; i < indexOrdering.size(); i++) { if (orderingExpressionMatches(indexOrdering.get(i), targetExpression)) { found = i - nequals; break; } } if (found < 0) { allFound = false; if ((equalityColumns == null) || !equalityColumns.contains(targetExpression)) continue; } else if (found >= groupBy.size()) { // Ordered by this column, but after some other // stuff which will break up the group. Only // partially grouped. allFound = false; } if (found >= index.getNKeyColumns()) { index.setUsesAllColumns(true); } anyFound = true; } if (anyFound) { if (!allFound) return IndexScan.OrderEffectiveness.PARTIAL_GROUPED; else if (result == IndexScan.OrderEffectiveness.SORTED) return result; else return IndexScan.OrderEffectiveness.GROUPED; } } else if (queryGoal.getProjectDistinct() != null) { assert (queryGoal.getOrdering() == null); if (orderedForDistinct(index, queryGoal.getProjectDistinct(), indexOrdering, nequals)) { return IndexScan.OrderEffectiveness.SORTED; } } return result; } private OrderByExpression getIndexColumn(List<OrderByExpression> indexOrdering, int idx) { OrderByExpression indexColumn = null; if (idx < indexOrdering.size()) { indexColumn = indexOrdering.get(idx); if (indexColumn.getExpression() == null) indexColumn = null; // Index sorts by unknown column. } return indexColumn; } /** For use with a Distinct that gets added later. */ public boolean orderedForDistinct(Project projectDistinct, IndexScan index) { List<OrderByExpression> indexOrdering = index.getOrdering(); if (indexOrdering == null) return false; int nequals = index.getNEquality(); return orderedForDistinct(index, projectDistinct, indexOrdering, nequals); } protected boolean orderedForDistinct(IndexScan index, Project projectDistinct, List<OrderByExpression> indexOrdering, int nequals) { List<ExpressionNode> distinct = projectDistinct.getFields(); for (ExpressionNode targetExpression : distinct) { int found = -1; for (int i = nequals; i < indexOrdering.size(); i++) { if (orderingExpressionMatches(indexOrdering.get(i), targetExpression)) { found = i - nequals; break; } } if ((found < 0) || (found >= distinct.size())) { return false; } if (found >= index.getNKeyColumns()) { index.setUsesAllColumns(true); } } return true; } // Does the column expression coming from the index match the ORDER BY target, // allowing for column equivalences? protected boolean orderingExpressionMatches(OrderByExpression orderByExpression, ExpressionNode targetExpression) { ExpressionNode columnExpression = orderByExpression.getExpression(); if (columnExpression == null) return false; if (columnExpression.equals(targetExpression)) return true; if (!(columnExpression instanceof ColumnExpression) || !(targetExpression instanceof ColumnExpression)) return false; return getColumnEquivalencies().areEquivalent((ColumnExpression)columnExpression, (ColumnExpression)targetExpression); } protected EquivalenceFinder<ColumnExpression> getColumnEquivalencies() { return queryGoal.getQuery().getColumnEquivalencies(); } protected class UnboundFinder implements ExpressionVisitor { boolean found = false; public UnboundFinder() { } @Override public boolean visitEnter(ExpressionNode n) { return visit(n); } @Override public boolean visitLeave(ExpressionNode n) { return !found; } @Override public boolean visit(ExpressionNode n) { if (n instanceof ColumnExpression) { ColumnExpression columnExpression = (ColumnExpression)n; if (!boundTables.contains(columnExpression.getTable())) { found = true; return false; } } else if (n instanceof SubqueryExpression) { for (ColumnSource used : ((SubqueryExpression)n).getSubquery().getOuterTables()) { // Tables defined inside the subquery are okay, but ones from outside // need to be bound to eval as an expression. if (!boundTables.contains(used)) { found = true; return false; } } } return true; } } /** Does the given expression have references to tables that aren't bound? */ protected boolean constantOrBound(ExpressionNode expression) { UnboundFinder f = new UnboundFinder(); expression.accept(f); return !f.found; } /** Get an expression form of the given index column. */ protected static ExpressionNode getIndexExpression(IndexScan index, IndexColumn indexColumn) { return getColumnExpression(index.getLeafMostTable(), indexColumn.getColumn()); } /** Get an expression form of the given group column. */ protected static ExpressionNode getColumnExpression(TableSource leafMostTable, Column column) { Table indexTable = column.getTable(); for (TableSource table = leafMostTable; null != table; table = table.getParentTable()) { if (table.getTable().getTable() == indexTable) { return new ColumnExpression(table, column); } } return null; } /** Is the comparison operand what the index indexes? */ protected boolean indexExpressionMatches(ExpressionNode indexExpression, ExpressionNode comparisonOperand) { if (indexExpression.equals(comparisonOperand)) return true; if (!(indexExpression instanceof ColumnExpression) || !(comparisonOperand instanceof ColumnExpression)) return false; if (getColumnEquivalencies().areEquivalent((ColumnExpression)indexExpression, (ColumnExpression)comparisonOperand)) return true; // See if comparing against a result column of the subquery, // that is, a join to the subquery that we can push down. ColumnExpression comparisonColumn = (ColumnExpression)comparisonOperand; ColumnSource comparisonTable = comparisonColumn.getTable(); if (!(comparisonTable instanceof SubquerySource)) return false; Subquery subquery = ((SubquerySource)comparisonTable).getSubquery(); if (subquery != queryGoal.getQuery()) return false; PlanNode input = subquery.getQuery(); if (input instanceof ResultSet) input = ((ResultSet)input).getInput(); if (!(input instanceof Project)) return false; Project project = (Project)input; ExpressionNode insideExpression = project.getFields().get(comparisonColumn.getPosition()); return indexExpressionMatches(indexExpression, insideExpression); } /** Find the best index among the branches. */ public BaseScan pickBestScan() { logger.debug("Picking for {}", this); BaseScan bestScan = null; bestScan = pickFullText(); if (bestScan != null) { return bestScan; // TODO: Always wins for now. } if (tables.getGroup().getRejectedJoins() != null) { bestScan = pickBestGroupLoop(); } IntersectionEnumerator intersections = new IntersectionEnumerator(); Set<TableSource> required = tables.getRequired(); for (TableGroupJoinNode table : tables) { IndexScan tableIndex = pickBestIndex(table, required, intersections); if ((tableIndex != null) && ((bestScan == null) || (compare(tableIndex, bestScan) > 0))) bestScan = tableIndex; ExpressionsHKeyScan hKeyRow = pickHKeyRow(table, required); if ((hKeyRow != null) && ((bestScan == null) || (compare(hKeyRow, bestScan) > 0))) bestScan = hKeyRow; } bestScan = pickBestIntersection(bestScan, intersections); if (bestScan == null) { GroupScan groupScan = new GroupScan(tables.getGroup()); groupScan.setCostEstimate(estimateCost(groupScan)); bestScan = groupScan; } return bestScan; } private BaseScan pickBestIntersection(BaseScan previousBest, IntersectionEnumerator enumerator) { // filter out all leaves which are obviously bad if (previousBest != null) { CostEstimate previousBestCost = previousBest.getCostEstimate(); for (Iterator<SingleIndexScan> iter = enumerator.leavesIterator(); iter.hasNext(); ) { SingleIndexScan scan = iter.next(); CostEstimate scanCost = estimateIntersectionCost(scan); if (scanCost.compareTo(previousBestCost) > 0) { logger.debug("Not intersecting {} {}", scan, scanCost); iter.remove(); } } } Function<? super IndexScan,Void> hook = intersectionEnumerationHook; for (Iterator<IndexScan> iterator = enumerator.iterator(); iterator.hasNext(); ) { IndexScan intersectedIndex = iterator.next(); if (hook != null) hook.apply(intersectedIndex); setIntersectionConditions(intersectedIndex); intersectedIndex.setCovering(determineCovering(intersectedIndex)); intersectedIndex.setCostEstimate(estimateCost(intersectedIndex)); if (previousBest == null) { logger.debug("Selecting {}", intersectedIndex); previousBest = intersectedIndex; } else if (compare(intersectedIndex, previousBest) > 0) { logger.debug("Preferring {}", intersectedIndex); previousBest = intersectedIndex; } else { logger.debug("Rejecting {}", intersectedIndex); // If the scan costs alone are higher than the previous best cost, there's no way this scan or // any scan that uses it will be the best. Just remove the whole branch. if (intersectedIndex.getScanCostEstimate().compareTo(previousBest.getCostEstimate()) > 0) iterator.remove(); } } return previousBest; } private void setIntersectionConditions(IndexScan rawScan) { MultiIndexIntersectScan scan = (MultiIndexIntersectScan) rawScan; if (isAncestor(scan.getOutputIndexScan().getLeafMostTable(), scan.getSelectorIndexScan().getLeafMostTable())) { // More conditions up the same branch are safely implied by the output row. ConditionsCounter<ConditionExpression> counter = new ConditionsCounter<>(conditions.size()); scan.incrementConditionsCounter(counter); scan.setConditions(new ArrayList<>(counter.getCountedConditions())); } else { // Otherwise only those for the output row are safe and // conditions on another branch need to be checked again; scan.setConditions(scan.getOutputIndexScan().getConditions()); } } /** Is the given <code>rootTable</code> an ancestor of <code>leafTable</code>? */ private static boolean isAncestor(TableSource leafTable, TableSource rootTable) { do { if (leafTable == rootTable) return true; leafTable = leafTable.getParentTable(); } while (leafTable != null); return false; } private class IntersectionEnumerator extends MultiIndexEnumerator<ConditionExpression,IndexScan,SingleIndexScan> { @Override protected Collection<ConditionExpression> getLeafConditions(SingleIndexScan scan) { int skips = scan.getPeggedCount(); List<ConditionExpression> conditions = scan.getConditions(); if (conditions == null) return null; int nconds = conditions.size(); return ((skips) > 0 && (skips == nconds)) ? conditions : null; } @Override protected IndexScan intersect(IndexScan first, IndexScan second, int comparisons) { return new MultiIndexIntersectScan(first, second, comparisons); } @Override protected List<Column> getComparisonColumns(IndexScan first, IndexScan second) { EquivalenceFinder<ColumnExpression> equivs = getColumnEquivalencies(); List<ExpressionNode> firstOrdering = orderingCols(first); List<ExpressionNode> secondOrdering = orderingCols(second); int ncols = Math.min(firstOrdering.size(), secondOrdering.size()); List<Column> result = new ArrayList<>(ncols); for (int i=0; i < ncols; ++i) { ExpressionNode firstCol = firstOrdering.get(i); ExpressionNode secondCol = secondOrdering.get(i); if (!(firstCol instanceof ColumnExpression) || !(secondCol instanceof ColumnExpression)) break; if (!equivs.areEquivalent((ColumnExpression) firstCol, (ColumnExpression) secondCol)) break; result.add(((ColumnExpression)firstCol).getColumn()); } return result; } private List<ExpressionNode> orderingCols(IndexScan index) { List<ExpressionNode> result = index.getColumns(); return result.subList(index.getPeggedCount(), result.size()); } } /** Find the best index on the given table. * @param required Tables reachable from root via INNER joins and hence not nullable. */ public IndexScan pickBestIndex(TableGroupJoinNode node, Set<TableSource> required, IntersectionEnumerator enumerator) { TableSource table = node.getTable(); IndexScan bestIndex = null; // Can only consider single table indexes when table is not // nullable (required). If table is the optional part of a // LEFT join, can still consider compatible LEFT / RIGHT group // indexes, below. WHERE conditions are removed before this is // called, see GroupIndexGoal#setJoinConditions(). if (required.contains(table)) { for (TableIndex index : table.getTable().getTable().getIndexes()) { SingleIndexScan candidate = new SingleIndexScan(index, table, queryContext); bestIndex = betterIndex(bestIndex, candidate, enumerator); } } if ((table.getGroup() != null) && !hasOuterJoinNonGroupConditions(node)) { for (GroupIndex index : table.getGroup().getGroup().getIndexes()) { // The leaf must be used or else we'll get duplicates from a // scan (the indexed columns need not be root to leaf, making // ancestors discontiguous and duplicates hard to eliminate). if (index.leafMostTable() != table.getTable().getTable()) continue; TableSource rootTable = table; TableSource rootRequired = null, leafRequired = null; if (index.getJoinType() == JoinType.LEFT) { while (rootTable != null) { if (required.contains(rootTable)) { rootRequired = rootTable; if (leafRequired == null) leafRequired = rootTable; } else { if (leafRequired != null) { // Optional above required, not LEFT join compatible. leafRequired = null; break; } } if (index.rootMostTable() == rootTable.getTable().getTable()) break; rootTable = rootTable.getParentTable(); } // The root must be present, since a LEFT index // does not contain orphans. if ((rootTable == null) || (rootRequired != rootTable) || (leafRequired == null)) continue; } else { if (!required.contains(table)) continue; leafRequired = table; boolean optionalSeen = false; while (rootTable != null) { if (required.contains(rootTable)) { if (optionalSeen) { // Required above optional, not RIGHT join compatible. rootRequired = null; break; } rootRequired = rootTable; } else { optionalSeen = true; } if (index.rootMostTable() == rootTable.getTable().getTable()) break; rootTable = rootTable.getParentTable(); } // TODO: There are no INNER JOIN group indexes, // but this would support them. /* if (optionalSeen && (index.getJoinType() == JoinType.INNER)) continue; */ if ((rootTable == null) || (rootRequired == null)) continue; } SingleIndexScan candidate = new SingleIndexScan(index, rootTable, rootRequired, leafRequired, table, queryContext); bestIndex = betterIndex(bestIndex, candidate, enumerator); } } return bestIndex; } // If a LEFT join has more conditions, they won't be included in an index, so // can't use it. protected boolean hasOuterJoinNonGroupConditions(TableGroupJoinNode node) { if (node.getTable().isRequired()) return false; ConditionList conditions = node.getJoinConditions(); if (conditions != null) { for (ConditionExpression cond : conditions) { if (cond.getImplementation() != ConditionExpression.Implementation.GROUP_JOIN) { return true; } } } return false; } protected IndexScan betterIndex(IndexScan bestIndex, SingleIndexScan candidate, IntersectionEnumerator enumerator) { if (usable(candidate)) { enumerator.addLeaf(candidate); if (bestIndex == null) { logger.debug("Selecting {}", candidate); return candidate; } else if (compare(candidate, bestIndex) > 0) { logger.debug("Preferring {}", candidate); return candidate; } else { logger.debug("Rejecting {}", candidate); } } return bestIndex; } private GroupLoopScan pickBestGroupLoop() { GroupLoopScan bestScan = null; Set<TableSource> outsideSameGroup = new HashSet<>(tables.getGroup().getTables()); outsideSameGroup.retainAll(boundTables); for (TableGroupJoin join : tables.getGroup().getRejectedJoins()) { TableSource parent = join.getParent(); TableSource child = join.getChild(); TableSource inside, outside; boolean insideIsParent; if (outsideSameGroup.contains(parent) && tables.containsTable(child)) { inside = child; outside = parent; insideIsParent = false; } else if (outsideSameGroup.contains(child) && tables.containsTable(parent)) { inside = parent; outside = child; insideIsParent = true; } else { continue; } if (mightFlattenOrSort(outside)) { continue; // Lookup_Nested won't be allowed. } GroupLoopScan forJoin = new GroupLoopScan(inside, outside, insideIsParent, join.getConditions()); determineRequiredTables(forJoin); forJoin.setCostEstimate(estimateCost(forJoin)); if (bestScan == null) { logger.debug("Selecting {}", forJoin); bestScan = forJoin; } else if (compare(forJoin, bestScan) > 0) { logger.debug("Preferring {}", forJoin); bestScan = forJoin; } else { logger.debug("Rejecting {}", forJoin); } } return bestScan; } private boolean mightFlattenOrSort(TableSource table) { if (!(table.getOutput() instanceof TableGroupJoinTree)) return true; // Don't know; be conservative. TableGroupJoinTree tree = (TableGroupJoinTree)table.getOutput(); TableGroupJoinNode root = tree.getRoot(); if (root.getTable() != table) return true; if (root.getFirstChild() != null) return true; // Only table in this join tree, shouldn't flatten. PlanNode output = tree; do { output = output.getOutput(); if (output instanceof Sort) return true; if (output instanceof ResultSet) break; } while (output != null); return false; // No Sort, either. } private ExpressionsHKeyScan pickHKeyRow(TableGroupJoinNode node, Set<TableSource> required) { TableSource table = node.getTable(); if (!required.contains(table)) return null; ExpressionsHKeyScan scan = new ExpressionsHKeyScan(table); HKey hKey = scan.getHKey(); int ncols = hKey.nColumns(); List<ExpressionNode> columns = new ArrayList<>(ncols); for (int i = 0; i < ncols; i++) { ExpressionNode column = getColumnExpression(table, hKey.column(i)); if (column == null) return null; columns.add(column); } scan.setColumns(columns); int nequals = insertLeadingEqualities(scan, conditions); if (nequals != ncols) return null; required = new HashSet<>(required); // We do not handle any actual data columns. required.addAll(requiredColumns.getTables()); scan.setRequiredTables(required); scan.setCostEstimate(estimateCost(scan)); logger.debug("Selecting {}", scan); return scan; } public int compare(BaseScan i1, BaseScan i2) { return i2.getCostEstimate().compareTo(i1.getCostEstimate()); } protected boolean determineCovering(IndexScan index) { // Include the non-condition requirements. RequiredColumns requiredAfter = new RequiredColumns(requiredColumns); RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredAfter); // Add in any conditions not handled by the index. for (ConditionExpression condition : conditions) { boolean found = false; if (index.getConditions() != null) { for (ConditionExpression indexCondition : index.getConditions()) { if (indexCondition == condition) { found = true; break; } } } if (!found) condition.accept(filler); } // Add sort if not handled by the index. if ((queryGoal.getOrdering() != null) && (index.getOrderEffectiveness() != IndexScan.OrderEffectiveness.SORTED)) { // Only this node, not its inputs. filler.setIncludedPlanNodes(Collections.<PlanNode>singletonList(queryGoal.getOrdering())); queryGoal.getOrdering().accept(filler); } // Record what tables are required: within the index if any // columns still needed, others if joined at all. Do this // before taking account of columns from a covering index, // since may not use it that way. { Collection<TableSource> joined = index.getTables(); Set<TableSource> required = new HashSet<>(); boolean moreTables = false; for (TableSource table : requiredAfter.getTables()) { if (!joined.contains(table)) { moreTables = true; required.add(table); } else if (requiredAfter.hasColumns(table) || (table == queryGoal.getUpdateTarget())) { required.add(table); } } index.setRequiredTables(required); if (moreTables) // Need to join up last the index; index might point // to an orphan. return false; } if (queryGoal.getUpdateTarget() != null) { // UPDATE statements need the whole target row and are thus never // covering for their group. for (TableGroupJoinNode table : tables) { if (table.getTable() == queryGoal.getUpdateTarget()) return false; } } // Remove the columns we do have from the index. int ncols = index.getColumns().size(); for (int i = 0; i < ncols; i++) { ExpressionNode column = index.getColumns().get(i); if ((column instanceof ColumnExpression) && index.isRecoverableAt(i)) { if (requiredAfter.have((ColumnExpression)column) && (i >= index.getNKeyColumns())) { index.setUsesAllColumns(true); } } } return requiredAfter.isEmpty(); } protected void determineRequiredTables(GroupLoopScan scan) { // Include the non-condition requirements. RequiredColumns requiredAfter = new RequiredColumns(requiredColumns); RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredAfter); // Add in any non-join conditions. for (ConditionExpression condition : conditions) { boolean found = false; for (ConditionExpression joinCondition : scan.getJoinConditions()) { if (joinCondition == condition) { found = true; break; } } if (!found) condition.accept(filler); } // Does not sort. if (queryGoal.getOrdering() != null) { // Only this node, not its inputs. filler.setIncludedPlanNodes(Collections.<PlanNode>singletonList(queryGoal.getOrdering())); queryGoal.getOrdering().accept(filler); } // The only table we can exclude is the one initially joined to, in the case // where all the data comes from elsewhere on that branch. Set<TableSource> required = new HashSet<>(requiredAfter.getTables()); if ((required.size() > 1) && !requiredAfter.hasColumns(scan.getInsideTable())) required.remove(scan.getInsideTable()); scan.setRequiredTables(required); } public CostEstimate estimateCost(IndexScan index) { return estimateCost(index, queryGoal.getLimit()); } public CostEstimate estimateCost(IndexScan index, long limit) { PlanCostEstimator estimator = newEstimator(); Set<TableSource> requiredTables = index.getRequiredTables(); estimator.indexScan(index); if (!index.isCovering()) { estimator.flatten(tables, index.getLeafMostTable(), requiredTables); } Collection<ConditionExpression> unhandledConditions = new HashSet<>(requiredConditions); if (index.getConditions() != null) unhandledConditions.removeAll(index.getConditions()); if (!unhandledConditions.isEmpty()) { estimator.select(unhandledConditions, selectivityConditions(unhandledConditions, requiredTables)); } if (queryGoal.needSort(index.getOrderEffectiveness())) { estimator.sort(queryGoal.sortFields()); } estimator.setLimit(limit); return estimator.getCostEstimate(); } public CostEstimate estimateIntersectionCost(IndexScan index) { if (index.getOrderEffectiveness() == IndexScan.OrderEffectiveness.NONE) return index.getScanCostEstimate(); long limit = queryGoal.getLimit(); if (limit < 0) return index.getScanCostEstimate(); // There is a limit and this index looks to be sorted, so adjust for that // limit. Otherwise, the scan only cost, which includes all rows, will appear // too large compared to a limit-aware best plan. PlanCostEstimator estimator = newEstimator(); estimator.indexScan(index); estimator.setLimit(limit); return estimator.getCostEstimate(); } public CostEstimate estimateCost(GroupScan scan) { PlanCostEstimator estimator = newEstimator(); Set<TableSource> requiredTables = requiredColumns.getTables(); estimator.groupScan(scan, tables, requiredTables); if (!requiredConditions.isEmpty()) { estimator.select(requiredConditions, selectivityConditions(requiredConditions, requiredTables)); } estimator.setLimit(queryGoal.getLimit()); return estimator.getCostEstimate(); } public CostEstimate estimateCost(GroupLoopScan scan) { PlanCostEstimator estimator = newEstimator(); Set<TableSource> requiredTables = scan.getRequiredTables(); estimator.groupLoop(scan, tables, requiredTables); Collection<ConditionExpression> unhandledConditions = new HashSet<>(requiredConditions); addInnerJoinConditions(unhandledConditions, scan.getInsideTable()); unhandledConditions.removeAll(scan.getJoinConditions()); if (!unhandledConditions.isEmpty()) { estimator.select(unhandledConditions, selectivityConditions(unhandledConditions, requiredTables)); } if (queryGoal.needSort(IndexScan.OrderEffectiveness.NONE)) { estimator.sort(queryGoal.sortFields()); } estimator.setLimit(queryGoal.getLimit()); return estimator.getCostEstimate(); } private void addInnerJoinConditions(Collection<ConditionExpression> unhandledConditions, TableSource insideTable) { PlanWithInput output = insideTable.getOutput(); while (output != null && !(output instanceof BaseQuery)) { if (output instanceof JoinNode) { JoinNode joinNode = (JoinNode) output; if (joinNode.isInnerJoin()) { unhandledConditions.addAll(joinNode.getJoinConditions()); this.conditionSources.add(joinNode.getJoinConditions()); } // the join conditions are supposed to be pushed down as far as they can be at this point, so once we // hit a join node, we're done break; } output = output.getOutput(); } } public CostEstimate estimateCost(ExpressionsHKeyScan scan) { PlanCostEstimator estimator = newEstimator(); Set<TableSource> requiredTables = scan.getRequiredTables(); estimator.hKeyRow(scan); estimator.flatten(tables, scan.getTable(), requiredTables); Collection<ConditionExpression> unhandledConditions = new HashSet<>(requiredConditions); unhandledConditions.removeAll(scan.getConditions()); if (!unhandledConditions.isEmpty()) { estimator.select(unhandledConditions, selectivityConditions(unhandledConditions, requiredTables)); } if (queryGoal.needSort(IndexScan.OrderEffectiveness.NONE)) { estimator.sort(queryGoal.sortFields()); } estimator.setLimit(queryGoal.getLimit()); return estimator.getCostEstimate(); } public double estimateSelectivity(IndexScan index) { return queryGoal.getCostEstimator().conditionsSelectivity(selectivityConditions(index.getConditions(), index.getTables())); } // Conditions that might have a recognizable selectivity. protected SelectivityConditions selectivityConditions(Collection<ConditionExpression> conditions, Collection<TableSource> requiredTables) { SelectivityConditions result = new SelectivityConditions(); if (conditions == null) { return result; } for (ConditionExpression condition : conditions) { if (condition instanceof ComparisonCondition) { ComparisonCondition ccond = (ComparisonCondition)condition; if (ccond.getLeft() instanceof ColumnExpression) { ColumnExpression column = (ColumnExpression)ccond.getLeft(); if ((column.getColumn() != null) && requiredTables.contains(column.getTable()) && constantOrBound(ccond.getRight())) { result.addCondition(column, condition); } } } else if (condition instanceof InListCondition) { InListCondition incond = (InListCondition)condition; if (incond.getOperand() instanceof ColumnExpression) { ColumnExpression column = (ColumnExpression)incond.getOperand(); if ((column.getColumn() != null) && requiredTables.contains(column.getTable())) { boolean allConstant = true; for (ExpressionNode expr : incond.getExpressions()) { if (!constantOrBound(expr)) { allConstant = false; break; } } if (allConstant) { result.addCondition(column, condition); } } } } } return result; } // Recognize the case of a join that is only used for predication. // TODO: This is only covers the simplest case, namely an index that is unique // none of whose columns are actually used. public boolean semiJoinEquivalent(BaseScan scan) { if (scan instanceof SingleIndexScan) { SingleIndexScan indexScan = (SingleIndexScan)scan; if (indexScan.isCovering() && isUnique(indexScan) && requiredColumns.isEmpty()) { return true; } } return false; } // Does this scan return at most one row? protected boolean isUnique(SingleIndexScan indexScan) { List<ExpressionNode> equalityComparands = indexScan.getEqualityComparands(); if (equalityComparands == null) return false; int nequals = equalityComparands.size(); Index index = indexScan.getIndex(); if (index.isUnique() && (nequals >= index.getKeyColumns().size())) return true; if (index.isGroupIndex()) return false; Set<Column> equalityColumns = new HashSet<>(nequals); for (int i = 0; i < nequals; i++) { ExpressionNode equalityExpr = indexScan.getColumns().get(i); if (equalityExpr instanceof ColumnExpression) { equalityColumns.add(((ColumnExpression)equalityExpr).getColumn()); } } TableIndex tableIndex = (TableIndex)index; find_index: // Find a unique index all of whose columns are equaled. for (TableIndex otherIndex : tableIndex.getTable().getIndexes()) { if (!otherIndex.isUnique()) continue; for (IndexColumn otherColumn : otherIndex.getKeyColumns()) { if (!equalityColumns.contains(otherColumn.getColumn())) continue find_index; } return true; } return false; } public JoinAndIndexPicker.Plan.JoinableWithConditionsToRemove install(BaseScan scan, List<ConditionList> conditionSources, boolean sortAllowed, boolean copy) { TableGroupJoinTree result = tables; List<? extends ConditionExpression> conditionsToRemove; // Need to have more than one copy of this tree in the final result. if (copy) result = new TableGroupJoinTree(result.getRoot()); result.setScan(scan); this.sortAllowed = sortAllowed; if (scan instanceof IndexScan) { IndexScan indexScan = (IndexScan)scan; if (indexScan instanceof MultiIndexIntersectScan) { MultiIndexIntersectScan multiScan = (MultiIndexIntersectScan)indexScan; installOrdering(indexScan, multiScan.getOrdering(), multiScan.getPeggedCount(), multiScan.getComparisonFields()); } installConditions(indexScan.getConditions(), conditionSources); conditionsToRemove = indexScan.getConditions(); if (sortAllowed) queryGoal.installOrderEffectiveness(indexScan.getOrderEffectiveness()); } else { if (scan instanceof GroupLoopScan) { GroupLoopScan groupScan = (GroupLoopScan) scan; installConditions(groupScan.getJoinConditions(), conditionSources); conditionsToRemove = groupScan.getJoinConditions(); } else if (scan instanceof FullTextScan) { FullTextScan textScan = (FullTextScan)scan; installConditions(textScan.getConditions(), conditionSources); conditionsToRemove = textScan.getConditions(); if (conditions.isEmpty()) { textScan.setLimit((int)queryGoal.getLimit()); } } else if (scan instanceof ExpressionsHKeyScan) { ExpressionsHKeyScan hKeyScan = (ExpressionsHKeyScan) scan; installConditions(hKeyScan.getConditions(), conditionSources); conditionsToRemove = hKeyScan.getConditions(); } else { conditionsToRemove = new ConditionList(); } if (sortAllowed) queryGoal.installOrderEffectiveness(IndexScan.OrderEffectiveness.NONE); } return new JoinAndIndexPicker.Plan.JoinableWithConditionsToRemove(result, conditionsToRemove); } /** Change WHERE as a consequence of <code>index</code> being * used, using either the sources returned by {@link #updateContext} or the * current ones if nothing has been changed. */ public void installConditions(Collection<? extends ConditionExpression> conditions, List<ConditionList> conditionSources) { if (conditions != null) { if (conditionSources == null) conditionSources = this.conditionSources; for (ConditionExpression condition : conditions) { for (ConditionList conditionSource : conditionSources) { if (conditionSource.remove(condition)) break; } } } } @Override public String toString() { StringBuilder str = new StringBuilder(); str.append(tables.summaryString(PlanNode.SummaryConfiguration.DEFAULT)); str.append("\n"); str.append(conditions); str.append("\n["); boolean first = true; for (ColumnSource bound : boundTables) { if (first) first = false; else str.append(", "); str.append(bound.getName()); } str.append("]"); return str.toString(); } // Too-many-way UNION can consume too many resources (and overflow // the stack explaining). protected static int COLUMN_RANGE_MAX_SEGMENTS_DEFAULT = 16; // Get Range-expressible conditions for given column. protected ColumnRanges rangeForIndex(ExpressionNode expressionNode) { if (expressionNode instanceof ColumnExpression) { if (columnsToRanges == null) { columnsToRanges = new HashMap<>(); for (ConditionExpression condition : conditions) { ColumnRanges range = ColumnRanges.rangeAtNode(condition); if (range != null) { ColumnExpression rangeColumn = range.getColumnExpression(); ColumnRanges oldRange = columnsToRanges.get(rangeColumn); if (oldRange != null) range = ColumnRanges.andRanges(range, oldRange); columnsToRanges.put(rangeColumn, range); } } if (!columnsToRanges.isEmpty()) { int maxSegments; String prop = queryGoal.getRulesContext().getProperty("columnRangeMaxSegments"); if (prop != null) maxSegments = Integer.parseInt(prop); else maxSegments = COLUMN_RANGE_MAX_SEGMENTS_DEFAULT; Iterator<ColumnRanges> iter = columnsToRanges.values().iterator(); while (iter.hasNext()) { if (iter.next().getSegments().size() > maxSegments) { iter.remove(); } } } } ColumnExpression columnExpression = (ColumnExpression)expressionNode; return columnsToRanges.get(columnExpression); } return null; } static class RequiredColumns { private Map<TableSource,Set<ColumnExpression>> map; public RequiredColumns(TableGroupJoinTree tables) { map = new HashMap<>(); for (TableGroupJoinNode table : tables) { map.put(table.getTable(), new HashSet<ColumnExpression>()); } } public RequiredColumns(RequiredColumns other) { map = new HashMap<>(other.map.size()); for (Map.Entry<TableSource,Set<ColumnExpression>> entry : other.map.entrySet()) { map.put(entry.getKey(), new HashSet<>(entry.getValue())); } } public Set<TableSource> getTables() { return map.keySet(); } public boolean hasColumns(TableSource table) { Set<ColumnExpression> entry = map.get(table); if (entry == null) return false; return !entry.isEmpty(); } public boolean isEmpty() { boolean empty = true; for (Set<ColumnExpression> entry : map.values()) if (!entry.isEmpty()) return false; return empty; } public void require(ColumnExpression expr) { Set<ColumnExpression> entry = map.get(expr.getTable()); if (entry != null) entry.add(expr); } /** Opposite of {@link #require}: note that we have a source for this column. */ public boolean have(ColumnExpression expr) { Set<ColumnExpression> entry = map.get(expr.getTable()); if (entry != null) return entry.remove(expr); else return false; } public void clear() { for (Set<ColumnExpression> entry : map.values()) entry.clear(); } } static class RequiredColumnsFiller implements PlanVisitor, ExpressionVisitor { private RequiredColumns requiredColumns; private Map<PlanNode,Void> excludedPlanNodes, includedPlanNodes; private Map<ExpressionNode,Void> excludedExpressions; private Deque<Boolean> excludeNodeStack = new ArrayDeque<>(); private boolean excludeNode = false; private int excludeDepth = 0; private int subqueryDepth = 0; public RequiredColumnsFiller(RequiredColumns requiredColumns) { this.requiredColumns = requiredColumns; } public RequiredColumnsFiller(RequiredColumns requiredColumns, Collection<PlanNode> excludedPlanNodes, Collection<ConditionExpression> excludedExpressions) { this.requiredColumns = requiredColumns; this.excludedPlanNodes = new IdentityHashMap<>(); for (PlanNode planNode : excludedPlanNodes) this.excludedPlanNodes.put(planNode, null); this.excludedExpressions = new IdentityHashMap<>(); for (ConditionExpression condition : excludedExpressions) this.excludedExpressions.put(condition, null); } public void setIncludedPlanNodes(Collection<PlanNode> includedPlanNodes) { this.includedPlanNodes = new IdentityHashMap<>(); for (PlanNode planNode : includedPlanNodes) this.includedPlanNodes.put(planNode, null); } @Override public boolean visitEnter(PlanNode n) { // Input nodes are called within the context of their output. // We want to know whether just this node is excluded, not // it and all its inputs. excludeNodeStack.push(excludeNode); excludeNode = exclude(n); if ((n instanceof Subquery) && !((Subquery)n).getOuterTables().isEmpty()) // TODO: Might be accessing tables from outer query as // group joins, which we don't support currently. Make // sure those aren't excluded. subqueryDepth++; return visit(n); } @Override public boolean visitLeave(PlanNode n) { excludeNode = excludeNodeStack.pop(); if ((n instanceof Subquery) && !((Subquery)n).getOuterTables().isEmpty()) subqueryDepth--; return true; } @Override public boolean visit(PlanNode n) { return true; } @Override public boolean visitEnter(ExpressionNode n) { if (!excludeNode && exclude(n)) excludeDepth++; return visit(n); } @Override public boolean visitLeave(ExpressionNode n) { if (!excludeNode && exclude(n)) excludeDepth--; return true; } @Override public boolean visit(ExpressionNode n) { if (!excludeNode && (excludeDepth == 0)) { if (n instanceof ColumnExpression) requiredColumns.require((ColumnExpression)n); } return true; } // Should this plan node be excluded from the requirement? protected boolean exclude(PlanNode node) { if (includedPlanNodes != null) return !includedPlanNodes.containsKey(node); else if (excludedPlanNodes != null) return excludedPlanNodes.containsKey(node); else return false; } // Should this expression be excluded from requirement? protected boolean exclude(ExpressionNode expr) { return (((excludedExpressions != null) && excludedExpressions.containsKey(expr)) || // Group join conditions are handled specially. ((expr instanceof ConditionExpression) && (((ConditionExpression)expr).getImplementation() == ConditionExpression.Implementation.GROUP_JOIN) && // Include expressions in subqueries until do joins across them. (subqueryDepth == 0))); } } /* Spatial indexes */ /** For now, a spatial index is a special kind of table index on * Z-order of two coordinates. */ public boolean spatialUsable(SingleIndexScan index, int nequals) { // Look for a spatial-join compatible predicate one of whose operands // matches the spatial index definition. // There are two additional legacy cases to recognize (for the time being): // WHERE distance_lat_lon(column_lat, column_lon, start_lat, start_lon) <= radius // ORDER BY znear(column_lat, column_lon, start_lat, start_lon), which // means fan out from that center in Z-order. ExpressionNode nextColumn = index.getColumns().get(nequals); if (!(nextColumn instanceof SpecialIndexExpression)) return false; // Did not have enough equalities to get to spatial part. SpecialIndexExpression indexExpression = (SpecialIndexExpression)nextColumn; SpecialIndexExpression.Function spatialFunction = indexExpression.getFunction(); List<ExpressionNode> operands = indexExpression.getOperands(); boolean matched = false; for (ConditionExpression condition : conditions) { ExpressionNode spatialJoinTo = null; if (condition instanceof FunctionCondition) { FunctionCondition fcond = (FunctionCondition)condition; spatialJoinTo = matchSpatialPredicate(spatialFunction, operands, fcond); } else if (condition instanceof ComparisonCondition) { ComparisonCondition ccond = (ComparisonCondition)condition; switch (ccond.getOperation()) { case LE: case LT: spatialJoinTo = matchDistanceLatLon(spatialFunction, operands, ccond.getLeft(), ccond.getRight()); break; case GE: case GT: spatialJoinTo = matchDistanceLatLon(spatialFunction, operands, ccond.getRight(), ccond.getLeft()); break; } } if (spatialJoinTo != null) { index.setLowComparand(spatialJoinTo, true); index.setHighComparand(spatialJoinTo, true); index.setOrderEffectiveness(IndexScan.OrderEffectiveness.NONE); matched = true; break; } } if (!matched) { if (sortAllowed && (queryGoal.getOrdering() != null)) { List<OrderByExpression> orderBy = queryGoal.getOrdering().getOrderBy(); if (orderBy.size() == 1) { ExpressionNode spatialJoinTo = matchZnear(spatialFunction, operands, orderBy.get(0)); if (spatialJoinTo != null) { index.setLowComparand(spatialJoinTo, true); index.setOrderEffectiveness(IndexScan.OrderEffectiveness.SORTED); matched = true; } } } if (!matched) return false; } index.setCovering(determineCovering(index)); index.setCostEstimate(estimateCostSpatial(index)); return true; } private ExpressionNode matchSpatialPredicate(SpecialIndexExpression.Function indexFunction, List<ExpressionNode> indexExpressions, FunctionCondition cond) { String function = cond.getFunction(); List<ExpressionNode> operands = cond.getOperands(); for (GeoOverlaps.OverlapType overlap : GeoOverlaps.OverlapType.values()) { if (function.equalsIgnoreCase(overlap.functionName())) { ExpressionNode op1 = operands.get(0); ExpressionNode op2 = operands.get(1); if (matchSpatialIndex(indexFunction, indexExpressions, op1) && constantOrBound(op2)) { return op2; } else if (matchSpatialIndex(indexFunction, indexExpressions, op2) && constantOrBound(op1)) { return op1; } } } if (function.equalsIgnoreCase("geo_within_distance")) { ExpressionNode op1 = operands.get(0); ExpressionNode op2 = operands.get(1); ExpressionNode radius = operands.get(2); if (constantOrBound(radius)) { if (matchSpatialIndex(indexFunction, indexExpressions, op1) && constantOrBound(op2)) { return spatialWithinDistance(op2, radius); } if (matchSpatialIndex(indexFunction, indexExpressions, op2) && constantOrBound(op1)) { return spatialWithinDistance(op1, radius); } } } return null; } private boolean matchSpatialIndex(SpecialIndexExpression.Function indexFunction, List<ExpressionNode> indexExpressions, ExpressionNode expr) { if (!(expr instanceof FunctionExpression)) return false; FunctionExpression func = (FunctionExpression)expr; return (func.getFunction().equalsIgnoreCase(indexFunction.functionName()) && func.getOperands().equals(indexExpressions)); } private ExpressionNode spatialWithinDistance(ExpressionNode op, ExpressionNode distance) { return new FunctionExpression("geo_expanded_envelope", Arrays.asList(op, distance), null, null, null); } private ExpressionNode spatialPoint(ExpressionNode lat, ExpressionNode lon) { return new FunctionExpression("geo_lat_lon", Arrays.asList(lat, lon), null, null, null); } private ExpressionNode matchDistanceLatLon(SpecialIndexExpression.Function function, List<ExpressionNode> indexExpressions, ExpressionNode left, ExpressionNode right) { if (!((function == SpecialIndexExpression.Function.GEO_LAT_LON) && (left instanceof FunctionExpression) && ((FunctionExpression)left).getFunction().equalsIgnoreCase("distance_lat_lon") && constantOrBound(right))) return null; ExpressionNode col1 = indexExpressions.get(0); ExpressionNode col2 = indexExpressions.get(1); List<ExpressionNode> operands = ((FunctionExpression)left).getOperands(); if (operands.size() != 4) return null; // TODO: Would error here be better? ExpressionNode op1 = operands.get(0); ExpressionNode op2 = operands.get(1); ExpressionNode op3 = operands.get(2); ExpressionNode op4 = operands.get(3); /* TODO: Should not be needed. if ((right.getType() != null) && (right.getType().typeClass().jdbcType() != Types.DECIMAL)) { DataTypeDescriptor sqlType = new DataTypeDescriptor(TypeId.DECIMAL_ID, 10, 6, true, 12); TInstance type = queryGoal.getRulesContext() .getTypesTranslator().typeForSQLType(sqlType); right = new CastExpression(right, sqlType, right.getSQLsource(), type); } */ if (columnMatches(col1, op1) && columnMatches(col2, op2) && constantOrBound(op3) && constantOrBound(op4)) { return spatialWithinDistance(spatialPoint(op3, op4), right); } if (columnMatches(col1, op3) && columnMatches(col2, op4) && constantOrBound(op1) && constantOrBound(op2)) { return spatialWithinDistance(spatialPoint(op1, op2), right); } return null; } private ExpressionNode matchZnear(SpecialIndexExpression.Function function, List<ExpressionNode> indexExpressions, OrderByExpression orderBy) { if (!((function == SpecialIndexExpression.Function.GEO_LAT_LON) && orderBy.isAscending())) return null; ExpressionNode orderExpr = orderBy.getExpression(); if (!((orderExpr instanceof FunctionExpression) && ((FunctionExpression)orderExpr).getFunction().equalsIgnoreCase("znear"))) return null; ExpressionNode col1 = indexExpressions.get(0); ExpressionNode col2 = indexExpressions.get(1); List<ExpressionNode> operands = ((FunctionExpression)orderExpr).getOperands(); if (operands.size() != 4) return null; // TODO: Would error here be better? ExpressionNode op1 = operands.get(0); ExpressionNode op2 = operands.get(1); ExpressionNode op3 = operands.get(2); ExpressionNode op4 = operands.get(3); if (columnMatches(col1, op1) && columnMatches(col2, op2) && constantOrBound(op3) && constantOrBound(op4)) return new FunctionExpression("geo_lat_lon", Arrays.asList(op3, op4), null, null, null); if (columnMatches(col1, op3) && columnMatches(col2, op4) && constantOrBound(op1) && constantOrBound(op2)) return new FunctionExpression("geo_lat_lon", Arrays.asList(op1, op2), null, null, null); return null; } private static boolean columnMatches(ExpressionNode col, ExpressionNode op) { if (op instanceof CastExpression) op = ((CastExpression)op).getOperand(); return col.equals(op); } public CostEstimate estimateCostSpatial(SingleIndexScan index) { PlanCostEstimator estimator = newEstimator(); Set<TableSource> requiredTables = requiredColumns.getTables(); estimator.spatialIndex(index); if (!index.isCovering()) { estimator.flatten(tables, index.getLeafMostTable(), requiredTables); } Collection<ConditionExpression> unhandledConditions = new HashSet<>(requiredConditions); if (index.getConditions() != null) unhandledConditions.removeAll(index.getConditions()); if (!unhandledConditions.isEmpty()) { estimator.select(unhandledConditions, selectivityConditions(unhandledConditions, requiredTables)); } if (queryGoal.needSort(index.getOrderEffectiveness())) { estimator.sort(queryGoal.sortFields()); } estimator.setLimit(queryGoal.getLimit()); return estimator.getCostEstimate(); } protected FullTextScan pickFullText() { List<ConditionExpression> textConditions = new ArrayList<>(0); for (ConditionExpression condition : conditions) { if ((condition instanceof FunctionExpression) && ((FunctionExpression)condition).getFunction().equalsIgnoreCase("full_text_search")) { textConditions.add(condition); } } if (textConditions.isEmpty()) return null; List<FullTextField> textFields = new ArrayList<>(0); FullTextQuery query = null; for (ConditionExpression condition : textConditions) { List<ExpressionNode> operands = ((FunctionExpression)condition).getOperands(); FullTextQuery clause = null; switch (operands.size()) { case 1: clause = fullTextBoolean(operands.get(0), textFields); if (clause == null) continue; break; case 2: if ((operands.get(0) instanceof ColumnExpression) && constantOrBound(operands.get(1))) { ColumnExpression column = (ColumnExpression)operands.get(0); if (column.getTable() instanceof TableSource) { if (!tables.containsTable((TableSource)column.getTable())) continue; FullTextField field = new FullTextField(column, FullTextField.Type.PARSE, operands.get(1)); textFields.add(field); clause = field; } } break; } if (clause == null) throw new UnsupportedSQLException("Unrecognized FULL_TEXT_SEARCH call", condition.getSQLsource()); if (query == null) { query = clause; } else { query = fullTextBoolean(Arrays.asList(query, clause), Arrays.asList(FullTextQueryBuilder.BooleanType.MUST, FullTextQueryBuilder.BooleanType.MUST)); } } if (query == null) return null; FullTextIndex foundIndex = null; TableSource foundTable = null; find_index: for (FullTextIndex index : textFields.get(0).getColumn().getColumn().getTable().getFullTextIndexes()) { TableSource indexTable = null; for (FullTextField textField : textFields) { Column column = textField.getColumn().getColumn(); boolean found = false; for (IndexColumn indexColumn : index.getKeyColumns()) { if (indexColumn.getColumn() == column) { if (foundIndex == null) { textField.setIndexColumn(indexColumn); } found = true; if ((indexTable == null) && (indexColumn.getColumn().getTable() == index.getIndexedTable())) { indexTable = (TableSource)textField.getColumn().getTable(); } break; } } if (!found) { continue find_index; } } if (foundIndex == null) { foundIndex = index; foundTable = indexTable; } else { throw new UnsupportedSQLException("Ambiguous full text index: " + foundIndex + " and " + index); } } if (foundIndex == null) { StringBuilder str = new StringBuilder("No full text index for: "); boolean first = true; for (FullTextField textField : textFields) { if (first) first = false; else str.append(", "); str.append(textField.getColumn()); } throw new UnsupportedSQLException(str.toString()); } if (foundTable == null) { for (TableGroupJoinNode node : tables) { if (node.getTable().getTable().getTable() == foundIndex.getIndexedTable()) { foundTable = node.getTable(); break; } } } query = normalizeFullTextQuery(query); FullTextScan scan = new FullTextScan(foundIndex, query, foundTable, textConditions); determineRequiredTables(scan); scan.setCostEstimate(estimateCostFullText(scan)); return scan; } protected FullTextQuery fullTextBoolean(ExpressionNode condition, List<FullTextField> textFields) { if (condition instanceof ComparisonCondition) { ComparisonCondition ccond = (ComparisonCondition)condition; if ((ccond.getLeft() instanceof ColumnExpression) && constantOrBound(ccond.getRight())) { ColumnExpression column = (ColumnExpression)ccond.getLeft(); if (column.getTable() instanceof TableSource) { if (!tables.containsTable((TableSource)column.getTable())) return null; FullTextField field = new FullTextField(column, FullTextField.Type.MATCH, ccond.getRight()); textFields.add(field); switch (ccond.getOperation()) { case EQ: return field; case NE: return fullTextBoolean(Arrays.<FullTextQuery>asList(field), Arrays.asList(FullTextQueryBuilder.BooleanType.NOT)); } } } } else if (condition instanceof LogicalFunctionCondition) { LogicalFunctionCondition lcond = (LogicalFunctionCondition)condition; String op = lcond.getFunction(); if ("and".equals(op)) { FullTextQuery left = fullTextBoolean(lcond.getLeft(), textFields); FullTextQuery right = fullTextBoolean(lcond.getRight(), textFields); if ((left == null) && (right == null)) return null; if ((left != null) && (right != null)) return fullTextBoolean(Arrays.asList(left, right), Arrays.asList(FullTextQueryBuilder.BooleanType.MUST, FullTextQueryBuilder.BooleanType.MUST)); } else if ("or".equals(op)) { FullTextQuery left = fullTextBoolean(lcond.getLeft(), textFields); FullTextQuery right = fullTextBoolean(lcond.getRight(), textFields); if ((left == null) && (right == null)) return null; if ((left != null) && (right != null)) return fullTextBoolean(Arrays.asList(left, right), Arrays.asList(FullTextQueryBuilder.BooleanType.SHOULD, FullTextQueryBuilder.BooleanType.SHOULD)); } else if ("not".equals(op)) { FullTextQuery inner = fullTextBoolean(lcond.getOperand(), textFields); if (inner == null) return null; else return fullTextBoolean(Arrays.asList(inner), Arrays.asList(FullTextQueryBuilder.BooleanType.NOT)); } } // TODO: LIKE throw new UnsupportedSQLException("Cannot convert to full text query" + condition); } protected FullTextQuery fullTextBoolean(List<FullTextQuery> operands, List<FullTextQueryBuilder.BooleanType> types) { // Make modifiable copies for normalize. return new FullTextBoolean(new ArrayList<>(operands), new ArrayList<>(types)); } protected FullTextQuery normalizeFullTextQuery(FullTextQuery query) { if (query instanceof FullTextBoolean) { FullTextBoolean bquery = (FullTextBoolean)query; List<FullTextQuery> operands = bquery.getOperands(); List<FullTextQueryBuilder.BooleanType> types = bquery.getTypes(); int i = 0; while (i < operands.size()) { FullTextQuery opQuery = operands.get(i); opQuery = normalizeFullTextQuery(opQuery); if (opQuery instanceof FullTextBoolean) { FullTextBoolean opbquery = (FullTextBoolean)opQuery; List<FullTextQuery> opOperands = opbquery.getOperands(); List<FullTextQueryBuilder.BooleanType> opTypes = opbquery.getTypes(); // Fold in the simplest cases: // [MUST(x), [MUST(y), MUST(z)]] -> [MUST(x), MUST(y), MUST(z)] // [MUST(x), [NOT(y)]] -> [MUST(x), NOT(y)] // [SHOULD(x), [SHOULD(y), SHOULD(z)]] -> [SHOULD(x), SHOULD(y), SHOULD(z)] boolean fold = true; switch (types.get(i)) { case MUST: check_must: for (FullTextQueryBuilder.BooleanType opType : opTypes) { switch (opType) { case MUST: case NOT: break; default: fold = false; break check_must; } } break; case SHOULD: check_should: for (FullTextQueryBuilder.BooleanType opType : opTypes) { switch (opType) { case SHOULD: break; default: fold = false; break check_should; } } break; default: fold = false; break; } if (fold) { for (int j = 0; j < opOperands.size(); j++) { FullTextQuery opOperand = opOperands.get(j); FullTextQueryBuilder.BooleanType opType = opTypes.get(j); if (j == 0) { operands.set(i, opOperand); types.set(i, opType); } else { operands.add(i, opOperand); types.add(i, opType); } i++; } continue; } } operands.set(i, opQuery); i++; } } return query; } protected void determineRequiredTables(FullTextScan scan) { // Include the non-condition requirements. RequiredColumns requiredAfter = new RequiredColumns(requiredColumns); RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredAfter); // Add in any non-full-text conditions. for (ConditionExpression condition : conditions) { boolean found = false; for (ConditionExpression scanCondition : scan.getConditions()) { if (scanCondition == condition) { found = true; break; } } if (!found) condition.accept(filler); } // Does not sort. if (queryGoal.getOrdering() != null) { // Only this node, not its inputs. filler.setIncludedPlanNodes(Collections.<PlanNode>singletonList(queryGoal.getOrdering())); queryGoal.getOrdering().accept(filler); } Set<TableSource> required = new HashSet<>(requiredAfter.getTables()); scan.setRequiredTables(required); } public CostEstimate estimateCostFullText(FullTextScan scan) { PlanCostEstimator estimator = newEstimator(); estimator.fullTextScan(scan); return estimator.getCostEstimate(); } protected PlanCostEstimator newEstimator() { return new PlanCostEstimator(queryGoal.getCostEstimator()); } }