All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.phoenix.execute.HashJoinPlan Maven / Gradle / Ivy

There is a newer version: 5.1.0-HBase-2.0.0.2
Show newest version
/*
 * 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.execute;

import static org.apache.phoenix.monitoring.TaskExecutionMetricsHolder.NO_OP_INSTANCE;
import static org.apache.phoenix.util.LogUtil.addCustomAnnotations;
import static org.apache.phoenix.util.NumberUtil.add;
import static org.apache.phoenix.util.NumberUtil.getMin;

import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.phoenix.cache.ServerCacheClient.ServerCache;
import org.apache.phoenix.compile.ColumnProjector;
import org.apache.phoenix.compile.ExplainPlan;
import org.apache.phoenix.compile.FromCompiler;
import org.apache.phoenix.compile.QueryPlan;
import org.apache.phoenix.compile.RowProjector;
import org.apache.phoenix.compile.ScanRanges;
import org.apache.phoenix.compile.StatementContext;
import org.apache.phoenix.compile.WhereCompiler;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.expression.Determinism;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.InListExpression;
import org.apache.phoenix.expression.LiteralExpression;
import org.apache.phoenix.expression.RowValueConstructorExpression;
import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.apache.phoenix.iterate.FilterResultIterator;
import org.apache.phoenix.iterate.ParallelScanGrouper;
import org.apache.phoenix.iterate.ResultIterator;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.job.JobManager.JobCallable;
import org.apache.phoenix.join.HashCacheClient;
import org.apache.phoenix.join.HashJoinInfo;
import org.apache.phoenix.monitoring.TaskExecutionMetricsHolder;
import org.apache.phoenix.parse.FilterableStatement;
import org.apache.phoenix.parse.ParseNode;
import org.apache.phoenix.parse.SQLParser;
import org.apache.phoenix.parse.SelectStatement;
import org.apache.phoenix.query.ConnectionQueryServices;
import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.query.QueryServicesOptions;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.tuple.Tuple;
import org.apache.phoenix.schema.types.PArrayDataType;
import org.apache.phoenix.schema.types.PBoolean;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.schema.types.PVarbinary;
import org.apache.phoenix.util.SQLCloseables;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

public class HashJoinPlan extends DelegateQueryPlan {
    private static final Log LOG = LogFactory.getLog(HashJoinPlan.class);

    private final SelectStatement statement;
    private final HashJoinInfo joinInfo;
    private final SubPlan[] subPlans;
    private final boolean recompileWhereClause;
    private final Set tableRefs;
    private final int maxServerCacheTimeToLive;
    private final Map dependencies = Maps.newHashMap();
    private HashCacheClient hashClient;
    private AtomicLong firstJobEndTime;
    private List keyRangeExpressions;
    private Long estimatedRows;
    private Long estimatedBytes;
    private Long estimateInfoTs;
    private boolean explainPlanCalled;
    
    public static HashJoinPlan create(SelectStatement statement, 
            QueryPlan plan, HashJoinInfo joinInfo, SubPlan[] subPlans) throws SQLException {
        if (!(plan instanceof HashJoinPlan))
            return new HashJoinPlan(statement, plan, joinInfo, subPlans, joinInfo == null, Collections.emptyMap());
        
        HashJoinPlan hashJoinPlan = (HashJoinPlan) plan;
        assert (hashJoinPlan.joinInfo == null && hashJoinPlan.delegate instanceof BaseQueryPlan);
        SubPlan[] mergedSubPlans = new SubPlan[hashJoinPlan.subPlans.length + subPlans.length];
        int i = 0;
        for (SubPlan subPlan : hashJoinPlan.subPlans) {
            mergedSubPlans[i++] = subPlan;
        }
        for (SubPlan subPlan : subPlans) {
            mergedSubPlans[i++] = subPlan;
        }
        return new HashJoinPlan(statement, hashJoinPlan.delegate, joinInfo, mergedSubPlans, true, hashJoinPlan.dependencies);
    }
    
    private HashJoinPlan(SelectStatement statement, 
            QueryPlan plan, HashJoinInfo joinInfo, SubPlan[] subPlans, boolean recompileWhereClause, Map dependencies) throws SQLException {
        super(plan);
        this.dependencies.putAll(dependencies);
        this.statement = statement;
        this.joinInfo = joinInfo;
        this.subPlans = subPlans;
        this.recompileWhereClause = recompileWhereClause;
        this.tableRefs = Sets.newHashSetWithExpectedSize(subPlans.length + plan.getSourceRefs().size());
        this.tableRefs.addAll(plan.getSourceRefs());
        for (SubPlan subPlan : subPlans) {
            tableRefs.addAll(subPlan.getInnerPlan().getSourceRefs());
        }
        this.maxServerCacheTimeToLive = plan.getContext().getConnection().getQueryServices().getProps().getInt(
                QueryServices.MAX_SERVER_CACHE_TIME_TO_LIVE_MS_ATTRIB, QueryServicesOptions.DEFAULT_MAX_SERVER_CACHE_TIME_TO_LIVE_MS);
    }
    
    @Override
    public Set getSourceRefs() {
        return tableRefs;
    }
        
    @Override
    public ResultIterator iterator(ParallelScanGrouper scanGrouper, Scan scan) throws SQLException {
        if (scan == null) {
            scan = delegate.getContext().getScan();
        }
        
        int count = subPlans.length;
        PhoenixConnection connection = getContext().getConnection();
        ConnectionQueryServices services = connection.getQueryServices();
        ExecutorService executor = services.getExecutor();
        List> futures = Lists.newArrayListWithExpectedSize(count);
        if (joinInfo != null) {
            hashClient = hashClient != null ? 
                    hashClient 
                  : new HashCacheClient(delegate.getContext().getConnection());
            firstJobEndTime = new AtomicLong(0);
            keyRangeExpressions = new CopyOnWriteArrayList();
        }
        
        for (int i = 0; i < count; i++) {
            final int index = i;
            futures.add(executor.submit(new JobCallable() {

                @Override
                public ServerCache call() throws Exception {
                    ServerCache cache = subPlans[index].execute(HashJoinPlan.this);
                    return cache;
                }

                @Override
                public Object getJobId() {
                    return HashJoinPlan.this;
                }

                @Override
                public TaskExecutionMetricsHolder getTaskExecutionMetric() {
                    return NO_OP_INSTANCE;
                }
            }));
        }
        
        SQLException firstException = null;
        for (int i = 0; i < count; i++) {
            try {
                ServerCache result = futures.get(i).get();
                if (result != null) {
                    dependencies.put(new ImmutableBytesPtr(result.getId()),result);
                }
                subPlans[i].postProcess(result, this);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                if (firstException == null) {
                    firstException = new SQLExceptionInfo.Builder(SQLExceptionCode.INTERRUPTED_EXCEPTION).setRootCause(e).setMessage("Sub plan [" + i + "] execution interrupted.").build().buildException();
                }
            } catch (ExecutionException e) {
                if (firstException == null) {
                    firstException = new SQLException("Encountered exception in sub plan [" + i + "] execution.", 
                            e.getCause());
                }
            }
        }
        if (firstException != null) {
            SQLCloseables.closeAllQuietly(dependencies.values());
            throw firstException;
        }
        
        Expression postFilter = null;
        boolean hasKeyRangeExpressions = keyRangeExpressions != null && !keyRangeExpressions.isEmpty();
        if (recompileWhereClause || hasKeyRangeExpressions) {
            StatementContext context = delegate.getContext();
            PTable table = context.getCurrentTable().getTable();
            ParseNode viewWhere = table.getViewStatement() == null ? null : new SQLParser(table.getViewStatement()).parseQuery().getWhere();
            context.setResolver(FromCompiler.getResolverForQuery((SelectStatement) (delegate.getStatement()), delegate.getContext().getConnection()));
            if (recompileWhereClause) {
                postFilter = WhereCompiler.compile(delegate.getContext(), delegate.getStatement(), viewWhere, null);
            }
            if (hasKeyRangeExpressions) {
                WhereCompiler.compile(delegate.getContext(), delegate.getStatement(), viewWhere, keyRangeExpressions, true, null);
            }
        }

        if (joinInfo != null) {
            HashJoinInfo.serializeHashJoinIntoScan(scan, joinInfo);
        }
        
        ResultIterator iterator = joinInfo == null ? delegate.iterator(scanGrouper, scan) : ((BaseQueryPlan) delegate).iterator(dependencies, scanGrouper, scan);
        if (statement.getInnerSelectStatement() != null && postFilter != null) {
            iterator = new FilterResultIterator(iterator, postFilter);
        }
        
        return iterator;
    }

    private Expression createKeyRangeExpression(Expression lhsExpression,
            Expression rhsExpression, List rhsValues, 
            ImmutableBytesWritable ptr, boolean rowKeyOrderOptimizable) throws SQLException {
        if (rhsValues.isEmpty())
            return LiteralExpression.newConstant(false, PBoolean.INSTANCE, Determinism.ALWAYS);        
        
        rhsValues.add(0, lhsExpression);
        
        return InListExpression.create(rhsValues, false, ptr, rowKeyOrderOptimizable);
    }

    @Override
    public ExplainPlan getExplainPlan() throws SQLException {
        explainPlanCalled = true;
        List planSteps = Lists.newArrayList(delegate.getExplainPlan().getPlanSteps());
        int count = subPlans.length;
        for (int i = 0; i < count; i++) {
            planSteps.addAll(subPlans[i].getPreSteps(this));
        }
        for (int i = 0; i < count; i++) {
            planSteps.addAll(subPlans[i].getPostSteps(this));
        }
        
        if (joinInfo != null && joinInfo.getPostJoinFilterExpression() != null) {
            planSteps.add("    AFTER-JOIN SERVER FILTER BY " + joinInfo.getPostJoinFilterExpression().toString());
        }
        if (joinInfo != null && joinInfo.getLimit() != null) {
            planSteps.add("    JOIN-SCANNER " + joinInfo.getLimit() + " ROW LIMIT");
        }
        for (SubPlan subPlan : subPlans) {
            if (subPlan.getInnerPlan().getEstimatedBytesToScan() == null
                    || subPlan.getInnerPlan().getEstimatedRowsToScan() == null
                    || subPlan.getInnerPlan().getEstimateInfoTimestamp() == null) {
                /*
                 * If any of the sub plans doesn't have the estimate info available, then we don't
                 * provide estimate for the overall plan
                 */
                estimatedBytes = null;
                estimatedRows = null;
                estimateInfoTs = null;
                break;
            } else {
                estimatedBytes =
                        add(estimatedBytes, subPlan.getInnerPlan().getEstimatedBytesToScan());
                estimatedRows = add(estimatedRows, subPlan.getInnerPlan().getEstimatedRowsToScan());
                estimateInfoTs =
                        getMin(estimateInfoTs, subPlan.getInnerPlan().getEstimateInfoTimestamp());
            }
        }
        return new ExplainPlan(planSteps);
    }

    @Override
    public FilterableStatement getStatement() {
        return statement;
    }

    protected interface SubPlan {
        public ServerCache execute(HashJoinPlan parent) throws SQLException;
        public void postProcess(ServerCache result, HashJoinPlan parent) throws SQLException;
        public List getPreSteps(HashJoinPlan parent) throws SQLException;
        public List getPostSteps(HashJoinPlan parent) throws SQLException;
        public QueryPlan getInnerPlan();
    }
    
    public static class WhereClauseSubPlan implements SubPlan {
        private final QueryPlan plan;
        private final SelectStatement select;
        private final boolean expectSingleRow;
        
        public WhereClauseSubPlan(QueryPlan plan, SelectStatement select, boolean expectSingleRow) {
            this.plan = plan;
            this.select = select;
            this.expectSingleRow = expectSingleRow;
        }

        @Override
        public ServerCache execute(HashJoinPlan parent) throws SQLException {
            List values = Lists. newArrayList();
            ResultIterator iterator = plan.iterator();
            try {
                RowProjector projector = plan.getProjector();
                ImmutableBytesWritable ptr = new ImmutableBytesWritable();
                int columnCount = projector.getColumnCount();
                int rowCount = 0;
                PDataType baseType = PVarbinary.INSTANCE;
                for (Tuple tuple = iterator.next(); tuple != null; tuple = iterator.next()) {
                    if (expectSingleRow && rowCount >= 1)
                        throw new SQLExceptionInfo.Builder(SQLExceptionCode.SINGLE_ROW_SUBQUERY_RETURNS_MULTIPLE_ROWS).build().buildException();

                    if (columnCount == 1) {
                        ColumnProjector columnProjector = projector.getColumnProjector(0);
                        baseType = columnProjector.getExpression().getDataType();
                        Object value = columnProjector.getValue(tuple, baseType, ptr);
                        values.add(value);
                    } else {
                        List expressions = Lists.newArrayListWithExpectedSize(columnCount);
                        for (int i = 0; i < columnCount; i++) {
                            ColumnProjector columnProjector = projector.getColumnProjector(i);
                            PDataType type = columnProjector.getExpression().getDataType();
                            Object value = columnProjector.getValue(tuple, type, ptr);
                            expressions.add(LiteralExpression.newConstant(value, type));
                        }
                        Expression expression = new RowValueConstructorExpression(expressions, true);
                        baseType = expression.getDataType();
                        expression.evaluate(null, ptr);
                        values.add(baseType.toObject(ptr));
                    }
                    rowCount++;
                }

                Object result = expectSingleRow ? (values.isEmpty() ? null : values.get(0)) : PArrayDataType.instantiatePhoenixArray(baseType, values.toArray());
                parent.getContext().setSubqueryResult(select, result);
                return null;
            } finally {
                iterator.close();
            }
        }

        @Override
        public void postProcess(ServerCache result, HashJoinPlan parent) throws SQLException {
        }

        @Override
        public List getPreSteps(HashJoinPlan parent) throws SQLException {
            List steps = Lists.newArrayList();
            steps.add("    EXECUTE " + (expectSingleRow ? "SINGLE" : "MULTIPLE") + "-ROW SUBQUERY");
            for (String step : plan.getExplainPlan().getPlanSteps()) {
                steps.add("        " + step);
            }
            return steps;
        }

        @Override
        public List getPostSteps(HashJoinPlan parent) throws SQLException {
            return Collections.emptyList();
        }

        @Override
        public QueryPlan getInnerPlan() {
            return plan;
        }
    }
    
    public static class HashSubPlan implements SubPlan {        
        private final int index;
        private final QueryPlan plan;
        private final List hashExpressions;
        private final boolean singleValueOnly;
        private final Expression keyRangeLhsExpression;
        private final Expression keyRangeRhsExpression;
        
        public HashSubPlan(int index, QueryPlan subPlan, 
                List hashExpressions,
                boolean singleValueOnly,
                Expression keyRangeLhsExpression, 
                Expression keyRangeRhsExpression) {
            this.index = index;
            this.plan = subPlan;
            this.hashExpressions = hashExpressions;
            this.singleValueOnly = singleValueOnly;
            this.keyRangeLhsExpression = keyRangeLhsExpression;
            this.keyRangeRhsExpression = keyRangeRhsExpression;
        }

        @Override
        public ServerCache execute(HashJoinPlan parent) throws SQLException {
            ScanRanges ranges = parent.delegate.getContext().getScanRanges();
            List keyRangeRhsValues = null;
            if (keyRangeRhsExpression != null) {
                keyRangeRhsValues = Lists.newArrayList();
            }
            ServerCache cache = null;
            if (hashExpressions != null) {
                ResultIterator iterator = plan.iterator();
                try {
                    cache =
                            parent.hashClient.addHashCache(ranges, iterator,
                                plan.getEstimatedSize(), hashExpressions, singleValueOnly,
                                parent.delegate.getTableRef(), keyRangeRhsExpression,
                                keyRangeRhsValues);
                    long endTime = System.currentTimeMillis();
                    boolean isSet = parent.firstJobEndTime.compareAndSet(0, endTime);
                    if (!isSet && (endTime
                            - parent.firstJobEndTime.get()) > parent.maxServerCacheTimeToLive) {
                        LOG.warn(addCustomAnnotations(
                            "Hash plan [" + index
                                    + "] execution seems too slow. Earlier hash cache(s) might have expired on servers.",
                            parent.delegate.getContext().getConnection()));
                    }
                } finally {
                    iterator.close();
                }
            } else {
                assert (keyRangeRhsExpression != null);
                ResultIterator iterator = plan.iterator();
                try {
                    for (Tuple result = iterator.next(); result != null; result = iterator.next()) {
                        // Evaluate key expressions for hash join key range optimization.
                        keyRangeRhsValues.add(HashCacheClient.evaluateKeyExpression(
                            keyRangeRhsExpression, result, plan.getContext().getTempPtr()));
                    }
                } finally {
                    iterator.close();
                }
            }
            if (keyRangeRhsValues != null) {
                parent.keyRangeExpressions.add(parent.createKeyRangeExpression(keyRangeLhsExpression, keyRangeRhsExpression, keyRangeRhsValues, plan.getContext().getTempPtr(), plan.getContext().getCurrentTable().getTable().rowKeyOrderOptimizable()));
            }
            return cache;
        }

        @Override
        public void postProcess(ServerCache result, HashJoinPlan parent)
                throws SQLException {
            ServerCache cache = result;
            if (cache != null) {
                parent.joinInfo.getJoinIds()[index].set(cache.getId());
            }
        }

        @Override
        public List getPreSteps(HashJoinPlan parent) throws SQLException {
            List steps = Lists.newArrayList();
            boolean earlyEvaluation = parent.joinInfo.earlyEvaluation()[index];
            boolean skipMerge = parent.joinInfo.getSchemas()[index].getFieldCount() == 0;
            if (hashExpressions != null) {
                steps.add("    PARALLEL " + parent.joinInfo.getJoinTypes()[index].toString().toUpperCase()
                        + "-JOIN TABLE " + index + (earlyEvaluation ? "" : "(DELAYED EVALUATION)") + (skipMerge ? " (SKIP MERGE)" : ""));
            }
            else {
                steps.add("    SKIP-SCAN-JOIN TABLE " + index);
            }
            for (String step : plan.getExplainPlan().getPlanSteps()) {
                steps.add("        " + step);
            }
            return steps;
        }

        @Override
        public List getPostSteps(HashJoinPlan parent) throws SQLException {
            if (keyRangeLhsExpression == null)
                return Collections. emptyList();
            
            String step = "    DYNAMIC SERVER FILTER BY " + keyRangeLhsExpression.toString() 
                    + " IN (" + keyRangeRhsExpression.toString() + ")";
            return Collections. singletonList(step);
        }


        @Override
        public QueryPlan getInnerPlan() {
            return plan;
        }
    }

    @Override
    public Long getEstimatedRowsToScan() throws SQLException {
        if (!explainPlanCalled) {
            getExplainPlan();
        }
        return estimatedRows;
    }

    @Override
    public Long getEstimatedBytesToScan() throws SQLException {
        if (!explainPlanCalled) {
            getExplainPlan();
        }
        return estimatedBytes;
    }

    @Override
    public Long getEstimateInfoTimestamp() throws SQLException {
        if (!explainPlanCalled) {
            getExplainPlan();
        }
        return estimateInfoTs;
    }
}