blob: 0c073ce83e9eaf3526d3bc4d991c9c2f376bf535 [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.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.execute.TupleProjector;
import org.apache.phoenix.expression.CoerceExpression;
import org.apache.phoenix.expression.Determinism;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.LiteralExpression;
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.schema.PTable;
import org.apache.phoenix.schema.SortOrder;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
/**
* 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.
*
*/
public class OrderPreservingTracker {
public enum Ordering {ORDERED, UNORDERED};
public static class Info {
public final OrderPreserving orderPreserving;
public final int pkPosition;
public final int slotSpan;
public Info(int pkPosition) {
this.pkPosition = pkPosition;
this.orderPreserving = OrderPreserving.YES;
this.slotSpan = 1;
}
public Info(Info info, OrderPreserving orderPreserving) {
this.pkPosition = info.pkPosition;
this.slotSpan = info.slotSpan;
this.orderPreserving = orderPreserving;
}
public Info(Info info, int slotSpan, OrderPreserving orderPreserving) {
this.pkPosition = info.pkPosition;
this.slotSpan = slotSpan;
this.orderPreserving = orderPreserving;
}
}
private final StatementContext context;
private final TrackOrderPreservingExpressionVisitor visitor;
private final GroupBy groupBy;
private final Ordering ordering;
private final int pkPositionOffset;
private final List<Info> orderPreservingInfos;
private final TupleProjector projector;
private boolean isOrderPreserving = true;
private Boolean isReverse = null;
private int orderPreservingColumnCount = 0;
public OrderPreservingTracker(StatementContext context, GroupBy groupBy, Ordering ordering, int nNodes) {
this(context, groupBy, ordering, nNodes, null);
}
public OrderPreservingTracker(StatementContext context, GroupBy groupBy, Ordering ordering, int nNodes, TupleProjector projector) {
this.context = context;
if (groupBy.isEmpty()) {
PTable table = context.getResolver().getTables().get(0).getTable();
this.isOrderPreserving = table.rowKeyOrderOptimizable();
boolean isSalted = table.getBucketNum() != null;
boolean isMultiTenant = context.getConnection().getTenantId() != null && table.isMultiTenant();
boolean isSharedViewIndex = table.getViewIndexId() != null;
// TODO: util for this offset, as it's computed in numerous places
this.pkPositionOffset = (isSalted ? 1 : 0) + (isMultiTenant ? 1 : 0) + (isSharedViewIndex ? 1 : 0);
} else {
this.isOrderPreserving = true;
this.pkPositionOffset = 0;
}
this.groupBy = groupBy;
this.visitor = new TrackOrderPreservingExpressionVisitor(projector);
this.orderPreservingInfos = Lists.newArrayListWithExpectedSize(nNodes);
this.ordering = ordering;
this.projector = projector;
}
public void track(Expression node) {
SortOrder sortOrder = node.getSortOrder();
track(node, sortOrder, null);
}
public void track(Expression node, SortOrder sortOrder, Boolean isNullsLast) {
if (isOrderPreserving) {
Info info = node.accept(visitor);
if (info == null) {
isOrderPreserving = false;
} else {
// If the expression is sorted in a different order than the specified sort order
// then the expressions are not order preserving.
if (node.getSortOrder() != sortOrder) {
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 && node.isNullable()) {
if (!Boolean.valueOf(isNullsLast).equals(isReverse)) {
isOrderPreserving = false;
isReverse = false;
return;
}
}
orderPreservingInfos.add(info);
}
}
}
/*
* Only valid AFTER call to isOrderPreserving
*/
public int getOrderPreservingColumnCount() {
return orderPreservingColumnCount;
}
public boolean isOrderPreserving() {
if (!isOrderPreserving) {
return false;
}
if (ordering == Ordering.UNORDERED) {
// Sort by position
Collections.sort(orderPreservingInfos, 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 = pkPositionOffset - 1;
OrderPreserving prevOrderPreserving = OrderPreserving.YES;
for (int i = 0; i < orderPreservingInfos.size() && isOrderPreserving; i++) {
Info entry = orderPreservingInfos.get(i);
int pos = entry.pkPosition;
isOrderPreserving &= entry.orderPreserving != OrderPreserving.NO && prevOrderPreserving == OrderPreserving.YES && (pos == prevPos || pos - prevSlotSpan == prevPos || hasEqualityConstraints(prevPos+prevSlotSpan, pos));
prevPos = pos;
prevSlotSpan = entry.slotSpan;
prevOrderPreserving = entry.orderPreserving;
}
orderPreservingColumnCount = prevPos + prevSlotSpan;
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.
// FIXME: this only recognizes row key columns that are held constant, not all columns.
// FIXME: we should optimize out any GROUP BY or ORDER BY expression which is deemed to
// be held constant based on the WHERE clause.
if (!groupBy.isEmpty()) {
for (int pos = startPos; pos < endPos; pos++) {
IsConstantVisitor visitor = new IsConstantVisitor(this.projector, ranges);
List<Expression> groupByExpressions = groupBy.getExpressions();
if (pos >= groupByExpressions.size()) { // sanity check - shouldn't be necessary
return false;
}
Expression groupByExpression = groupByExpressions.get(pos);
if ( groupByExpression.getDeterminism().ordinal() > Determinism.PER_STATEMENT.ordinal() ) {
return false;
}
Boolean isConstant = groupByExpression.accept(visitor);
if (!Boolean.TRUE.equals(isConstant)) {
return false;
}
}
return true;
}
for (int pos = startPos; pos < endPos; pos++) {
if (!ranges.hasEqualityConstraint(pos)) {
return false;
}
}
return true;
}
public boolean isReverse() {
return Boolean.TRUE.equals(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 TupleProjector projector;
private final ScanRanges scanRanges;
public IsConstantVisitor(TupleProjector projector, ScanRanges scanRanges) {
this.projector = projector;
this.scanRanges = scanRanges;
}
@Override
public Boolean defaultReturn(Expression node, List<Boolean> returnValues) {
if (node.getDeterminism().ordinal() > Determinism.PER_STATEMENT.ordinal() ||
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(ProjectedColumnExpression node) {
if (projector == null) {
return super.visit(node);
}
Expression expression = projector.getExpressions()[node.getPosition()];
// Only look one level down the projection.
if (expression instanceof ProjectedColumnExpression) {
return super.visit(node);
}
return expression.accept(this);
}
}
/**
*
* 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 final TupleProjector projector;
public TrackOrderPreservingExpressionVisitor(TupleProjector projector) {
this.projector = projector;
}
@Override
public Info visit(RowKeyColumnExpression node) {
return new Info(node.getPosition());
}
@Override
public Info visit(ProjectedColumnExpression node) {
if (projector == null) {
return super.visit(node);
}
Expression expression = projector.getExpressions()[node.getPosition()];
// Only look one level down the projection.
if (expression instanceof ProjectedColumnExpression) {
return super.visit(node);
}
return expression.accept(this);
}
@Override
public Iterator<Expression> visitEnter(ScalarFunction node) {
return node.preservesOrder() == OrderPreserving.NO ? Iterators.<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())];
if (orderPreserving == info.orderPreserving) {
return info;
}
return new Info(info, orderPreserving);
}
@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; }
lastInfo = info;
}
return new Info(firstInfo, l.size(), lastInfo.orderPreserving);
}
}
}