blob: 96e4d817b5da983cd49a6f7f84ed024284a8ec60 [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.iterate;
import java.text.Format;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.apache.hadoop.hbase.client.Consistency;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter;
import org.apache.hadoop.hbase.filter.PageFilter;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.io.TimeRange;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.compile.OrderByCompiler.OrderBy;
import org.apache.phoenix.compile.ScanRanges;
import org.apache.phoenix.compile.StatementContext;
import org.apache.phoenix.coprocessor.BaseScannerRegionObserver;
import org.apache.phoenix.filter.BooleanExpressionFilter;
import org.apache.phoenix.filter.DistinctPrefixFilter;
import org.apache.phoenix.parse.HintNode;
import org.apache.phoenix.parse.HintNode.Hint;
import org.apache.phoenix.query.KeyRange;
import org.apache.phoenix.query.KeyRange.Bound;
import org.apache.phoenix.schema.RowKeySchema;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.schema.types.PInteger;
import org.apache.phoenix.util.ScanUtil;
import org.apache.phoenix.util.StringUtil;
import com.google.common.collect.Iterators;
public abstract class ExplainTable {
private static final List<KeyRange> EVERYTHING = Collections.singletonList(KeyRange.EVERYTHING_RANGE);
protected final StatementContext context;
protected final TableRef tableRef;
protected final GroupBy groupBy;
protected final OrderBy orderBy;
protected final HintNode hint;
protected final Integer limit;
protected final Integer offset;
public ExplainTable(StatementContext context, TableRef table) {
this(context, table, GroupBy.EMPTY_GROUP_BY, OrderBy.EMPTY_ORDER_BY, HintNode.EMPTY_HINT_NODE, null, null);
}
public ExplainTable(StatementContext context, TableRef table, GroupBy groupBy, OrderBy orderBy, HintNode hintNode,
Integer limit, Integer offset) {
this.context = context;
this.tableRef = table;
this.groupBy = groupBy;
this.orderBy = orderBy;
this.hint = hintNode;
this.limit = limit;
this.offset = offset;
}
private boolean explainSkipScan(StringBuilder buf) {
ScanRanges scanRanges = context.getScanRanges();
if (scanRanges.isPointLookup()) {
int keyCount = scanRanges.getPointLookupCount();
buf.append("POINT LOOKUP ON " + keyCount + " KEY" + (keyCount > 1 ? "S " : " "));
} else if (scanRanges.useSkipScanFilter()) {
buf.append("SKIP SCAN ");
int count = 1;
boolean hasRanges = false;
int nSlots = scanRanges.getBoundSlotCount();
for (int i = 0; i < nSlots; i++) {
List<KeyRange> ranges = scanRanges.getRanges().get(i);
count *= ranges.size();
for (KeyRange range : ranges) {
hasRanges |= !range.isSingleKey();
}
}
buf.append("ON ");
buf.append(count);
buf.append(hasRanges ? " RANGE" : " KEY");
buf.append(count > 1 ? "S " : " ");
} else {
buf.append("RANGE SCAN ");
}
return scanRanges.useSkipScanFilter();
}
protected void explain(String prefix, List<String> planSteps) {
StringBuilder buf = new StringBuilder(prefix);
ScanRanges scanRanges = context.getScanRanges();
Scan scan = context.getScan();
if (scan.getConsistency() != Consistency.STRONG){
buf.append("TIMELINE-CONSISTENCY ");
}
if (hint.hasHint(Hint.SMALL)) {
buf.append(Hint.SMALL).append(" ");
}
if (OrderBy.REV_ROW_KEY_ORDER_BY.equals(orderBy)) {
buf.append("REVERSE ");
}
if (scanRanges.isEverything()) {
buf.append("FULL SCAN ");
} else {
explainSkipScan(buf);
}
buf.append("OVER ").append(tableRef.getTable().getPhysicalName().getString());
if (!scanRanges.isPointLookup()) {
appendKeyRanges(buf);
}
planSteps.add(buf.toString());
if (context.getScan() != null && tableRef.getTable().getRowTimestampColPos() != -1) {
TimeRange range = context.getScan().getTimeRange();
planSteps.add(" ROW TIMESTAMP FILTER [" + range.getMin() + ", " + range.getMax() + ")");
}
PageFilter pageFilter = null;
FirstKeyOnlyFilter firstKeyOnlyFilter = null;
BooleanExpressionFilter whereFilter = null;
DistinctPrefixFilter distinctFilter = null;
Iterator<Filter> filterIterator = ScanUtil.getFilterIterator(scan);
if (filterIterator.hasNext()) {
do {
Filter filter = filterIterator.next();
if (filter instanceof FirstKeyOnlyFilter) {
firstKeyOnlyFilter = (FirstKeyOnlyFilter)filter;
} else if (filter instanceof PageFilter) {
pageFilter = (PageFilter)filter;
} else if (filter instanceof BooleanExpressionFilter) {
whereFilter = (BooleanExpressionFilter)filter;
} else if (filter instanceof DistinctPrefixFilter) {
distinctFilter = (DistinctPrefixFilter)filter;
}
} while (filterIterator.hasNext());
}
if (whereFilter != null) {
planSteps.add(" SERVER FILTER BY " + (firstKeyOnlyFilter == null ? "" : "FIRST KEY ONLY AND ") + whereFilter.toString());
} else if (firstKeyOnlyFilter != null) {
planSteps.add(" SERVER FILTER BY FIRST KEY ONLY");
}
if (distinctFilter != null) {
planSteps.add(" SERVER DISTINCT PREFIX FILTER OVER "+groupBy.getExpressions().toString());
}
if (!orderBy.getOrderByExpressions().isEmpty() && groupBy.isEmpty()) { // with GROUP BY, sort happens client-side
planSteps.add(" SERVER" + (limit == null ? "" : " TOP " + limit + " ROW" + (limit == 1 ? "" : "S"))
+ " SORTED BY " + orderBy.getOrderByExpressions().toString());
} else {
if (offset != null) {
planSteps.add(" SERVER OFFSET " + offset);
}
if (pageFilter != null) {
planSteps.add(" SERVER " + pageFilter.getPageSize() + " ROW LIMIT");
}
}
Integer groupByLimit = null;
byte[] groupByLimitBytes = scan.getAttribute(BaseScannerRegionObserver.GROUP_BY_LIMIT);
if (groupByLimitBytes != null) {
groupByLimit = (Integer) PInteger.INSTANCE.toObject(groupByLimitBytes);
}
groupBy.explain(planSteps, groupByLimit);
if (scan.getAttribute(BaseScannerRegionObserver.SPECIFIC_ARRAY_INDEX) != null) {
planSteps.add(" SERVER ARRAY ELEMENT PROJECTION");
}
}
private void appendPKColumnValue(StringBuilder buf, byte[] range, Boolean isNull, int slotIndex, boolean changeViewIndexId) {
if (Boolean.TRUE.equals(isNull)) {
buf.append("null");
return;
}
if (Boolean.FALSE.equals(isNull)) {
buf.append("not null");
return;
}
if (range.length == 0) {
buf.append('*');
return;
}
ScanRanges scanRanges = context.getScanRanges();
PDataType type = scanRanges.getSchema().getField(slotIndex).getDataType();
SortOrder sortOrder = tableRef.getTable().getPKColumns().get(slotIndex).getSortOrder();
if (sortOrder == SortOrder.DESC) {
buf.append('~');
ImmutableBytesWritable ptr = new ImmutableBytesWritable(range);
type.coerceBytes(ptr, type, sortOrder, SortOrder.getDefault());
range = ptr.get();
}
if (changeViewIndexId) {
Short s = (Short) type.toObject(range);
s = (short) (s + (-Short.MAX_VALUE));
buf.append(s.toString());
} else {
Format formatter = context.getConnection().getFormatter(type);
buf.append(type.toStringLiteral(range, formatter));
}
}
private static class RowKeyValueIterator implements Iterator<byte[]> {
private final RowKeySchema schema;
private ImmutableBytesWritable ptr = new ImmutableBytesWritable();
private int position = 0;
private final int maxOffset;
private byte[] nextValue;
public RowKeyValueIterator(RowKeySchema schema, byte[] rowKey) {
this.schema = schema;
this.maxOffset = schema.iterator(rowKey, ptr);
iterate();
}
private void iterate() {
if (schema.next(ptr, position++, maxOffset) == null) {
nextValue = null;
} else {
nextValue = ptr.copyBytes();
}
}
@Override
public boolean hasNext() {
return nextValue != null;
}
@Override
public byte[] next() {
if (nextValue == null) {
throw new NoSuchElementException();
}
byte[] value = nextValue;
iterate();
return value;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private void appendScanRow(StringBuilder buf, Bound bound) {
ScanRanges scanRanges = context.getScanRanges();
// TODO: review this and potentially intersect the scan ranges
// with the minMaxRange in ScanRanges to prevent having to do all this.
KeyRange minMaxRange = scanRanges.getMinMaxRange();
Iterator<byte[]> minMaxIterator = Iterators.emptyIterator();
if (minMaxRange != KeyRange.EVERYTHING_RANGE) {
RowKeySchema schema = tableRef.getTable().getRowKeySchema();
if (!minMaxRange.isUnbound(bound)) {
minMaxIterator = new RowKeyValueIterator(schema, minMaxRange.getRange(bound));
}
}
boolean isLocalIndex = ScanUtil.isLocalIndex(context.getScan());
boolean forceSkipScan = this.hint.hasHint(Hint.SKIP_SCAN);
int nRanges = forceSkipScan ? scanRanges.getRanges().size() : scanRanges.getBoundSlotCount();
for (int i = 0, minPos = 0; minPos < nRanges || minMaxIterator.hasNext(); i++) {
List<KeyRange> ranges = minPos >= nRanges ? EVERYTHING : scanRanges.getRanges().get(minPos++);
KeyRange range = bound == Bound.LOWER ? ranges.get(0) : ranges.get(ranges.size()-1);
byte[] b = range.getRange(bound);
Boolean isNull = KeyRange.IS_NULL_RANGE == range ? Boolean.TRUE : KeyRange.IS_NOT_NULL_RANGE == range ? Boolean.FALSE : null;
if (minMaxIterator.hasNext()) {
byte[] bMinMax = minMaxIterator.next();
int cmp = Bytes.compareTo(bMinMax, b) * (bound == Bound.LOWER ? 1 : -1);
if (cmp > 0) {
minPos = nRanges;
b = bMinMax;
isNull = null;
} else if (cmp < 0) {
minMaxIterator = Iterators.emptyIterator();
}
}
if (isLocalIndex && i == 0) {
appendPKColumnValue(buf, b, isNull, i, true);
} else {
appendPKColumnValue(buf, b, isNull, i, false);
}
buf.append(',');
}
}
private void appendKeyRanges(StringBuilder buf) {
ScanRanges scanRanges = context.getScanRanges();
if (scanRanges.isDegenerate() || scanRanges.isEverything()) {
return;
}
buf.append(" [");
StringBuilder buf1 = new StringBuilder();
appendScanRow(buf1, Bound.LOWER);
buf.append(buf1);
buf.setCharAt(buf.length()-1, ']');
StringBuilder buf2 = new StringBuilder();
appendScanRow(buf2, Bound.UPPER);
if (!StringUtil.equals(buf1, buf2)) {
buf.append( " - [");
buf.append(buf2);
}
buf.setCharAt(buf.length()-1, ']');
}
}