blob: 0435148ec9ed5725e564c33fe74a6cb29fef9b1e [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.phoenix.compile;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.compile.OrderByCompiler.OrderBy;
import org.apache.phoenix.execute.SortMergeJoinPlan;
import org.apache.phoenix.expression.CoerceExpression;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.KeyValueColumnExpression;
import org.apache.phoenix.expression.LiteralExpression;
import org.apache.phoenix.expression.OrderByExpression;
import org.apache.phoenix.expression.ProjectedColumnExpression;
import org.apache.phoenix.expression.RowKeyColumnExpression;
import org.apache.phoenix.expression.RowValueConstructorExpression;
import org.apache.phoenix.expression.function.FunctionExpression.OrderPreserving;
import org.apache.phoenix.expression.function.ScalarFunction;
import org.apache.phoenix.expression.visitor.StatelessTraverseAllExpressionVisitor;
import org.apache.phoenix.expression.visitor.StatelessTraverseNoExpressionVisitor;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.util.ExpressionUtil;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
/**
* <pre>
* Determines if the natural key order of the rows returned by the scan
* will match the order of the expressions. For GROUP BY, if order is preserved we can use
* an optimization during server-side aggregation to do the aggregation on-the-fly versus
* keeping track of each distinct group. We can only do this optimization if all the rows
* for each group will be contiguous. For ORDER BY, we can drop the ORDER BY statement if
* the order is preserved.
*
* There are mainly three changes for refactoring this class in PHOENIX-5148:
* 1.added a {@link #getInputOrderBys} method to determine the input OrderBys by the combination
* of innerQueryPlan's output OrderBys , GroupBy of current QueryPlan and the rowKeyColumns of current table.
*
* 2.because the innerQueryPlan may have multiple output OrderBys(for {@link SortMergeJoinPlan}),
* so I extracted many stateful member variables (such as orderPreservingTrackInfos, isOrderPreserving
* and isReverse etc.) to a new inner class {@link TrackOrderByContext}, {@link TrackOrderByContext}
* is used to track if a single Input OrderBy matches the target OrderByExpressions in {@link #isOrderPreserving()}
* method, and once we found a {@link TrackOrderByContext} satisfied {@link #isOrderPreserving()},
* {@link #selectedTrackOrderByContext} member variable is set to this {@link TrackOrderByContext},
* then we can use this {@link #selectedTrackOrderByContext} to implement the {@link #getOrderPreservingTrackInfos()}
* and {@link #isReverse()} methods etc.
* BTW. at most only one {@link TrackOrderByContext} can meet {@link #isOrderPreserving()} is true.
*
* 3.added ascending and nullsLast to the inner class {@link Info} , and extracted complete ordering
* information in {@link Info} class by the inner {@link TrackOrderPreservingExpressionVisitor} class,
* so we can inferring alignment between the target OrderByExpressions and the input OrderBys based on
* {@link Info} in {@link TrackOrderByContext#doTrack} method, not on the row keys like the original
* {@link #track} method does.
* </pre>
*/
public class OrderPreservingTracker {
public enum Ordering {ORDERED, UNORDERED};
public static class Info {
private final OrderPreserving orderPreserving;
private final int pkPosition;
private final int slotSpan;
private final boolean ascending;
private final boolean nullsLast;
private Expression expression;
public Info(int pkPosition, boolean ascending, boolean nullsLast) {
this.pkPosition = pkPosition;
this.orderPreserving = OrderPreserving.YES;
this.slotSpan = 1;
this.ascending = ascending;
this.nullsLast = nullsLast;
}
public Info(int rowKeyColumnPosition, int rowKeySlotSpan, OrderPreserving orderPreserving, boolean ascending, boolean nullsLast) {
this.pkPosition = rowKeyColumnPosition;
this.slotSpan = rowKeySlotSpan;
this.orderPreserving = orderPreserving;
this.ascending = ascending;
this.nullsLast = nullsLast;
}
public static List<Expression> extractExpressions(List<Info> orderPreservingTrackInfos) {
List<Expression> newExpressions = new ArrayList<Expression>(orderPreservingTrackInfos.size());
for(Info trackInfo : orderPreservingTrackInfos) {
newExpressions.add(trackInfo.expression);
}
return newExpressions;
}
public Expression getExpression() {
return expression;
}
public boolean isAscending() {
return ascending;
}
public boolean isNullsLast() {
return nullsLast;
}
}
private final StatementContext context;
private final GroupBy groupBy;
private final Ordering ordering;
private int pkPositionOffset = 0;
private Expression whereExpression;
private List<TrackOrderByCell> trackOrderByCells = new LinkedList<TrackOrderByCell>();
private List<TrackOrderByContext> trackOrderByContexts = Collections.<TrackOrderByContext> emptyList();
private TrackOrderByContext selectedTrackOrderByContext = null;
private List<OrderBy> inputOrderBys = Collections.<OrderBy> emptyList();
public OrderPreservingTracker(StatementContext context, GroupBy groupBy, Ordering ordering, int nNodes) throws SQLException {
this(context, groupBy, ordering, nNodes, null, null, null);
}
public OrderPreservingTracker(
StatementContext context,
GroupBy groupBy,
Ordering ordering,
int nNodes,
List<OrderBy> inputOrderBys,
QueryPlan innerQueryPlan,
Expression whereExpression) throws SQLException {
this.context = context;
boolean isOrderPreserving = false;
if (groupBy.isEmpty() && inputOrderBys == null) {
PTable table = context.getResolver().getTables().get(0).getTable();
isOrderPreserving = table.rowKeyOrderOptimizable();
} else {
isOrderPreserving = true;
}
this.groupBy = groupBy;
this.ordering = ordering;
this.whereExpression = whereExpression;
if(inputOrderBys != null) {
this.inputOrderBys = inputOrderBys;
} else {
this.getInputOrderBys(innerQueryPlan, groupBy, context);
}
if(this.inputOrderBys.isEmpty()) {
return;
}
this.trackOrderByContexts = new ArrayList<TrackOrderByContext>(this.inputOrderBys.size());
for(OrderBy inputOrderBy : this.inputOrderBys) {
this.trackOrderByContexts.add(
new TrackOrderByContext(isOrderPreserving, nNodes, inputOrderBy));
}
}
/**
* Infer input OrderBys, if the innerQueryPlan is null, we make the OrderBys from the pk columns of {@link PTable}.
* @param innerQueryPlan
* @param groupBy
* @param statementContext
* @throws SQLException
*/
private void getInputOrderBys(QueryPlan innerQueryPlan, GroupBy groupBy, StatementContext statementContext) throws SQLException {
if(!groupBy.isEmpty()) {
this.inputOrderBys = Collections.singletonList(ExpressionUtil.convertGroupByToOrderBy(groupBy, false));
return;
}
if(innerQueryPlan != null) {
this.inputOrderBys = innerQueryPlan.getOutputOrderBys();
return;
}
this.inputOrderBys = Collections.<OrderBy> emptyList();
TableRef tableRef = statementContext.getResolver().getTables().get(0);
PhoenixConnection phoenixConnection = statementContext.getConnection();
Pair<OrderBy,Integer> orderByAndRowKeyColumnOffset =
ExpressionUtil.getOrderByFromTable(tableRef, phoenixConnection, false);
OrderBy orderBy = orderByAndRowKeyColumnOffset.getFirst();
this.pkPositionOffset = orderByAndRowKeyColumnOffset.getSecond();
if(orderBy != OrderBy.EMPTY_ORDER_BY) {
this.inputOrderBys = Collections.singletonList(orderBy);
}
}
private class TrackOrderByContext {
private List<Info> orderPreservingTrackInfos;
private boolean isOrderPreserving = true;
private Boolean isReverse = null;
private int orderPreservingColumnCount = 0;
private final TrackOrderPreservingExpressionVisitor trackOrderPreservingExpressionVisitor;
private OrderBy inputOrderBy = null;
public TrackOrderByContext(boolean isOrderPreserving, int orderByNodeCount, OrderBy inputOrderBy) {
this.isOrderPreserving = isOrderPreserving;
this.trackOrderPreservingExpressionVisitor = new TrackOrderPreservingExpressionVisitor(inputOrderBy);
this.orderPreservingTrackInfos = Lists.newArrayListWithExpectedSize(orderByNodeCount);
this.inputOrderBy = inputOrderBy;
}
public void track(List<TrackOrderByCell> trackOrderByCells) {
for(TrackOrderByCell trackOrderByCell : trackOrderByCells) {
doTrack(trackOrderByCell.expression,
trackOrderByCell.isAscending,
trackOrderByCell.isNullsLast);
}
}
private void doTrack(Expression expression, Boolean isAscending, Boolean isNullsLast) {
if (!isOrderPreserving) {
return;
}
Info trackInfo = expression.accept(trackOrderPreservingExpressionVisitor);
if (trackInfo == null) {
isOrderPreserving = false;
return;
}
// If the expression is sorted in a different order than the specified sort order
// then the expressions are not order preserving.
if (isAscending != null && trackInfo.ascending != isAscending.booleanValue()) {
if (isReverse == null) {
isReverse = true;
} else if (!isReverse){
isOrderPreserving = false;
isReverse = false;
return;
}
} else {
if (isReverse == null) {
isReverse = false;
} else if (isReverse){
isOrderPreserving = false;
isReverse = false;
return;
}
}
if (isNullsLast!=null && expression.isNullable()) {
if ((trackInfo.nullsLast == isNullsLast.booleanValue()) && isReverse.booleanValue() ||
(trackInfo.nullsLast != isNullsLast.booleanValue()) && !isReverse.booleanValue()) {
isOrderPreserving = false;
isReverse = false;
return;
}
}
trackInfo.expression = expression;
orderPreservingTrackInfos.add(trackInfo);
}
/*
* Only valid AFTER call to isOrderPreserving
*/
public int getOrderPreservingColumnCount() {
return orderPreservingColumnCount;
}
/**
* Only valid AFTER call to isOrderPreserving
*/
public List<Info> getOrderPreservingTrackInfos() {
if(this.isOrderPreserving) {
return ImmutableList.copyOf(this.orderPreservingTrackInfos);
}
int orderPreservingColumnCountToUse = this.orderPreservingColumnCount - pkPositionOffset;
if(orderPreservingColumnCountToUse <= 0) {
return Collections.<Info> emptyList();
}
return ImmutableList.copyOf(this.orderPreservingTrackInfos.subList(0, orderPreservingColumnCountToUse));
}
public boolean isOrderPreserving() {
if (!isOrderPreserving) {
return false;
}
if (ordering == Ordering.UNORDERED) {
// Sort by position
Collections.sort(orderPreservingTrackInfos, new Comparator<Info>() {
@Override
public int compare(Info o1, Info o2) {
int cmp = o1.pkPosition-o2.pkPosition;
if (cmp != 0) return cmp;
// After pk position, sort on reverse OrderPreserving ordinal: NO, YES_IF_LAST, YES
// In this way, if we have an ORDER BY over a YES_IF_LAST followed by a YES, we'll
// allow subsequent columns to be ordered.
return o2.orderPreserving.ordinal() - o1.orderPreserving.ordinal();
}
});
}
// Determine if there are any gaps in the PK columns (in which case we don't need
// to sort in the coprocessor because the keys will already naturally be in sorted
// order.
int prevSlotSpan = 1;
int prevPos = -1;
OrderPreserving prevOrderPreserving = OrderPreserving.YES;
for (int i = 0; i < orderPreservingTrackInfos.size(); i++) {
Info entry = orderPreservingTrackInfos.get(i);
int pos = entry.pkPosition;
isOrderPreserving &= entry.orderPreserving != OrderPreserving.NO &&
prevOrderPreserving == OrderPreserving.YES &&
(pos == prevPos ||
pos - prevSlotSpan == prevPos ||
hasEqualityConstraints(prevPos+prevSlotSpan, pos));
if(!isOrderPreserving) {
break;
}
prevPos = pos;
prevSlotSpan = entry.slotSpan;
prevOrderPreserving = entry.orderPreserving;
}
orderPreservingColumnCount = prevPos + prevSlotSpan + pkPositionOffset;
return isOrderPreserving;
}
private boolean hasEqualityConstraints(int startPos, int endPos) {
ScanRanges ranges = context.getScanRanges();
// If a GROUP BY is being done, then the rows are ordered according to the GROUP BY key,
// not by the original row key order of the table (see PHOENIX-3451).
// We check each GROUP BY expression to see if it only references columns that are
// matched by equality constraints, in which case the expression itself would be constant.
for (int pos = startPos; pos < endPos; pos++) {
Expression expressionToCheckConstant = this.getExpressionToCheckConstant(pos);
IsConstantVisitor visitor = new IsConstantVisitor(ranges, whereExpression);
Boolean isConstant = expressionToCheckConstant.accept(visitor);
if (!Boolean.TRUE.equals(isConstant)) {
return false;
}
}
return true;
}
public boolean isReverse() {
return Boolean.TRUE.equals(isReverse);
}
private Expression getExpressionToCheckConstant(int columnIndex) {
if (!groupBy.isEmpty()) {
List<Expression> groupByExpressions = groupBy.getExpressions();
assert columnIndex < groupByExpressions.size();
return groupByExpressions.get(columnIndex);
}
assert columnIndex < inputOrderBy.getOrderByExpressions().size();
return inputOrderBy.getOrderByExpressions().get(columnIndex).getExpression();
}
}
private static class TrackOrderByCell
{
private Expression expression;
private Boolean isAscending;
private Boolean isNullsLast;
public TrackOrderByCell(Expression expression,Boolean isAscending, Boolean isNullsLast) {
this.expression = expression;
this.isAscending = isAscending;
this.isNullsLast = isNullsLast;
}
}
public void track(Expression expression) {
track(expression, null, null);
}
public void track(Expression expression, Boolean isAscending, Boolean isNullsLast) {
TrackOrderByCell trackOrderByContext =
new TrackOrderByCell(expression, isAscending, isNullsLast);
this.trackOrderByCells.add(trackOrderByContext);
}
/*
* Only valid AFTER call to isOrderPreserving
*/
public int getOrderPreservingColumnCount() {
if(this.selectedTrackOrderByContext == null) {
return 0;
}
return this.selectedTrackOrderByContext.getOrderPreservingColumnCount();
}
/**
* Only valid AFTER call to isOrderPreserving
*/
public List<Info> getOrderPreservingTrackInfos() {
if(this.selectedTrackOrderByContext == null) {
return Collections.<Info> emptyList();
}
return this.selectedTrackOrderByContext.getOrderPreservingTrackInfos();
}
public boolean isOrderPreserving() {
if(this.selectedTrackOrderByContext != null) {
throw new IllegalStateException("isOrderPreserving should be called only once");
}
if(this.trackOrderByContexts.isEmpty()) {
return false;
}
if(this.trackOrderByCells.isEmpty()) {
return false;
}
/**
* at most only one TrackOrderByContext can meet isOrderPreserving is true
*/
for(TrackOrderByContext trackOrderByContext : this.trackOrderByContexts) {
trackOrderByContext.track(trackOrderByCells);
if(trackOrderByContext.isOrderPreserving()) {
this.selectedTrackOrderByContext = trackOrderByContext;
break;
}
if(this.selectedTrackOrderByContext == null) {
this.selectedTrackOrderByContext = trackOrderByContext;
}
}
return this.selectedTrackOrderByContext.isOrderPreserving;
}
public boolean isReverse() {
if(this.selectedTrackOrderByContext == null) {
throw new IllegalStateException("isReverse should only be called when isOrderPreserving is true!");
}
return this.selectedTrackOrderByContext.isReverse();
}
/**
*
* Determines if an expression is held constant. Only works for columns in the PK currently,
* as we only track whether PK columns are held constant.
*
*/
private static class IsConstantVisitor extends StatelessTraverseAllExpressionVisitor<Boolean> {
private final ScanRanges scanRanges;
private final Expression whereExpression;
public IsConstantVisitor(ScanRanges scanRanges, Expression whereExpression) {
this.scanRanges = scanRanges;
this.whereExpression = whereExpression;
}
@Override
public Boolean defaultReturn(Expression node, List<Boolean> returnValues) {
if (!ExpressionUtil.isContantForStatement(node) ||
returnValues.size() < node.getChildren().size()) {
return Boolean.FALSE;
}
for (Boolean returnValue : returnValues) {
if (!returnValue) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
@Override
public Boolean visit(RowKeyColumnExpression node) {
return scanRanges.hasEqualityConstraint(node.getPosition());
}
@Override
public Boolean visit(LiteralExpression node) {
return Boolean.TRUE;
}
@Override
public Boolean visit(KeyValueColumnExpression keyValueColumnExpression) {
return ExpressionUtil.isColumnExpressionConstant(keyValueColumnExpression, whereExpression);
}
@Override
public Boolean visit(ProjectedColumnExpression projectedColumnExpression) {
return ExpressionUtil.isColumnExpressionConstant(projectedColumnExpression, whereExpression);
}
}
/**
*
* Visitor used to determine if order is preserved across a list of expressions (GROUP BY or ORDER BY expressions)
*
*/
private static class TrackOrderPreservingExpressionVisitor extends StatelessTraverseNoExpressionVisitor<Info> {
private Map<Expression, Pair<Integer,OrderByExpression>> expressionToPositionAndOrderByExpression;
public TrackOrderPreservingExpressionVisitor(OrderBy orderBy) {
if(orderBy.isEmpty()) {
this.expressionToPositionAndOrderByExpression = Collections.<Expression, Pair<Integer,OrderByExpression>> emptyMap();
return;
}
List<OrderByExpression> orderByExpressions = orderBy.getOrderByExpressions();
this.expressionToPositionAndOrderByExpression = new HashMap<Expression, Pair<Integer,OrderByExpression>>(orderByExpressions.size());
int index = 0;
for(OrderByExpression orderByExpression : orderByExpressions) {
this.expressionToPositionAndOrderByExpression.put(
orderByExpression.getExpression(),
new Pair<Integer,OrderByExpression>(index++, orderByExpression));
}
}
@Override
public Info defaultReturn(Expression expression, List<Info> childInfos) {
return match(expression);
}
@Override
public Info visit(RowKeyColumnExpression rowKeyColumnExpression) {
return match(rowKeyColumnExpression);
}
@Override
public Info visit(KeyValueColumnExpression keyValueColumnExpression) {
return match(keyValueColumnExpression);
}
@Override
public Info visit(ProjectedColumnExpression projectedColumnExpression) {
return match(projectedColumnExpression);
}
private Info match(Expression expression)
{
Pair<Integer,OrderByExpression> positionAndOrderByExpression = this.expressionToPositionAndOrderByExpression.get(expression);
if(positionAndOrderByExpression == null) {
return null;
}
return new Info(
positionAndOrderByExpression.getFirst(),
positionAndOrderByExpression.getSecond().isAscending(),
positionAndOrderByExpression.getSecond().isNullsLast());
}
@Override
public Iterator<Expression> visitEnter(ScalarFunction node) {
return node.preservesOrder() == OrderPreserving.NO ? Collections.<Expression> emptyIterator() : Iterators
.singletonIterator(node.getChildren().get(node.getKeyFormationTraversalIndex()));
}
@Override
public Info visitLeave(ScalarFunction node, List<Info> l) {
if (l.isEmpty()) { return null; }
Info info = l.get(0);
// Keep the minimum value between this function and the current value,
// so that we never increase OrderPreserving from NO or YES_IF_LAST.
OrderPreserving orderPreserving = OrderPreserving.values()[Math.min(node.preservesOrder().ordinal(), info.orderPreserving.ordinal())];
Expression childExpression = node.getChildren().get(
node.getKeyFormationTraversalIndex());
boolean sortOrderIsSame = node.getSortOrder() == childExpression.getSortOrder();
if (orderPreserving == info.orderPreserving && sortOrderIsSame) {
return info;
}
return new Info(
info.pkPosition,
info.slotSpan,
orderPreserving,
sortOrderIsSame ? info.ascending : !info.ascending,
info.nullsLast);
}
@Override
public Iterator<Expression> visitEnter(CoerceExpression node) {
return node.getChildren().iterator();
}
@Override
public Info visitLeave(CoerceExpression node, List<Info> l) {
if (l.isEmpty()) { return null; }
return l.get(0);
}
@Override
public Iterator<Expression> visitEnter(RowValueConstructorExpression node) {
return node.getChildren().iterator();
}
@Override
public Info visitLeave(RowValueConstructorExpression node, List<Info> l) {
// Child expression returned null and was filtered, so not order preserving
if (l.size() != node.getChildren().size()) { return null; }
Info firstInfo = l.get(0);
Info lastInfo = firstInfo;
// Check that pkPos are consecutive which is the only way a RVC can be order preserving
for (int i = 1; i < l.size(); i++) {
// not order preserving since it's not last
if (lastInfo.orderPreserving == OrderPreserving.YES_IF_LAST) { return null; }
Info info = l.get(i);
// not order preserving since there's a gap in the pk
if (info.pkPosition != lastInfo.pkPosition + 1) {
return null;
}
if(info.ascending != lastInfo.ascending) {
return null;
}
if(info.nullsLast != lastInfo.nullsLast) {
return null;
}
lastInfo = info;
}
return new Info(firstInfo.pkPosition, l.size(), lastInfo.orderPreserving, lastInfo.ascending, lastInfo.nullsLast);
}
}
}