Package org.voltdb.planner.microoptimizations

Source Code of org.voltdb.planner.microoptimizations.ReplaceWithIndexLimit

/* This file is part of VoltDB.
* Copyright (C) 2008-2014 VoltDB Inc.
*
* 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 VoltDB.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.voltdb.planner.microoptimizations;

import java.util.ArrayList;
import java.util.List;

import org.json_voltpatches.JSONException;
import org.voltdb.catalog.CatalogMap;
import org.voltdb.catalog.ColumnRef;
import org.voltdb.catalog.Index;
import org.voltdb.expressions.AbstractExpression;
import org.voltdb.expressions.ComparisonExpression;
import org.voltdb.expressions.ExpressionUtil;
import org.voltdb.expressions.TupleValueExpression;
import org.voltdb.planner.parseinfo.StmtTableScan;
import org.voltdb.plannodes.AbstractPlanNode;
import org.voltdb.plannodes.AbstractScanPlanNode;
import org.voltdb.plannodes.AggregatePlanNode;
import org.voltdb.plannodes.IndexScanPlanNode;
import org.voltdb.plannodes.LimitPlanNode;
import org.voltdb.plannodes.SeqScanPlanNode;
import org.voltdb.types.ExpressionType;
import org.voltdb.types.IndexLookupType;
import org.voltdb.types.IndexType;
import org.voltdb.types.SortDirectionType;
import org.voltdb.utils.CatalogUtil;

public class ReplaceWithIndexLimit extends MicroOptimization {

    // for debug purpose only, this might not be called
    int indent = 0;
    protected void recursivelyPrint(AbstractPlanNode node, StringBuilder sb)
    {
        for (int i = 0; i < indent; i++) {
            sb.append("\t");
        }
        sb.append(node.toJSONString() + "\n");
        indent++;
        if (node.getChildCount() > 0) {
            recursivelyPrint(node.getChild(0), sb);
        }
    }

    @Override
    protected AbstractPlanNode recursivelyApply(AbstractPlanNode plan)
    {
        assert(plan != null);

        // depth first:
        //     Find AggregatePlanNode with exactly one child
        //     where that child is an AbstractScanPlanNode.
        //     Replace qualifying SeqScanPlanNode with an
        //     IndexScanPlanNode with an inlined LimitPlanNode;
        //     or appending the LimitPlanNode to the existing
        //     qualified IndexScanPlanNode.

        ArrayList<AbstractPlanNode> children = new ArrayList<AbstractPlanNode>();

        for (int i = 0; i < plan.getChildCount(); i++)
            children.add(plan.getChild(i));

        for (AbstractPlanNode child : children) {
            // TODO this will break when children feed multiple parents
            AbstractPlanNode newChild = recursivelyApply(child);
            // Do a graft into the (parent) plan only if a replacement for a child was found.
            if (newChild == child) {
                continue;
            }
            child.removeFromGraph();
            plan.addAndLinkChild(newChild);
        }

        // check for an aggregation of the right form
        if ((plan instanceof AggregatePlanNode) == false)
            return plan;
        assert(plan.getChildCount() == 1);
        AggregatePlanNode aggplan = (AggregatePlanNode)plan;

        // handle one single min() / max() now
        // TODO: combination of [min(), max(), count()]
        SortDirectionType sortDirection = SortDirectionType.INVALID;

        if (aggplan.isTableMin()) {
            sortDirection = SortDirectionType.ASC;
        } else if (aggplan.isTableMax()) {
            sortDirection = SortDirectionType.DESC;
        } else {
            return plan;
        }

        AbstractPlanNode child = plan.getChild(0);
        AbstractExpression aggExpr = aggplan.getFirstAggregateExpression();

        /**
         * For generated SeqScan plan, look through all available indexes, if the first key
         * of any one index matches the min()/max(), use that index with an inlined LIMIT
         * node.
         * For generated IndexScan plan, verify current index can be used for min()/max(), if
         * so, appending an inlined LIMIT node to it.
         * To avoid further handling to locate aggExpr for partitioned table in upper AGGREGATOR
         * (coordinator), keep this old trivial AGGREGATOR node.
         */

        // for a SEQSCAN, replace it with a INDEXSCAN node with an inline LIMIT plan node
        if (child instanceof SeqScanPlanNode) {
            // only replace SeqScan when no predicate
            // should have other index access plan if any qualified index found for the predicate
            if (((SeqScanPlanNode)child).getPredicate() != null) {
                return plan;
            }

            if (((AbstractScanPlanNode)child).isSubQuery()) {
                return plan;
            }

            // create an empty bindingExprs list, used for store (possible) bindings for adHoc query
            ArrayList<AbstractExpression> bindings = new ArrayList<AbstractExpression>();
            Index ret = findQualifiedIndex(((SeqScanPlanNode)child), aggExpr, bindings);

            if (ret == null) {
                return plan;
            } else {
                // 1. create one INDEXSCAN plan node with inlined LIMIT
                // and replace the SEQSCAN node with it
                // 2. we know which end row we want to fetch, so it's safe to
                // specify sorting direction here
                IndexScanPlanNode ispn = new IndexScanPlanNode((SeqScanPlanNode) child, aggplan, ret, sortDirection);
                ispn.setBindings(bindings);
                assert(ispn.getSearchKeyExpressions().size() == 0);
                if (sortDirection == SortDirectionType.ASC) {
                    assert(aggplan.isTableMin());
                    ispn.setSkipNullPredicate(0);
                }

                LimitPlanNode lpn = new LimitPlanNode();
                lpn.setLimit(1);
                lpn.setOffset(0);

                ispn.addInlinePlanNode(lpn);

                // remove old SeqScan node and link the new generated IndexScan node
                plan.clearChildren();
                plan.addAndLinkChild(ispn);

                return plan;
            }
        }

        if ((child instanceof IndexScanPlanNode) == false) {
            return plan;
        }

        // already have the IndexScanPlanNode
        IndexScanPlanNode ispn = (IndexScanPlanNode)child;

        // can do optimization only if it has no (post-)predicates
        // except those (post-)predicates are artifact predicates
        // we added for reverse scan purpose only
        if (((IndexScanPlanNode)child).getPredicate() != null &&
                !((IndexScanPlanNode)child).isPredicatesOptimizableForAggregate()) {
            return plan;
        }

        // Guard against (possible future?) cases of indexable subquery.
        if (((AbstractScanPlanNode)child).isSubQuery()) {
            return plan;
        }

        // 1. Handle ALL equality filters case.
        // In the IndexScanPlanNode:
        //      -- EQFilterExprs were put in searchkeyExpressions and endExpressions
        //      -- startCondition is only in searchKeyExpressions
        //      -- endCondition is only in endExpressions
        // So, if the lookup type is EQ, then all filters must be equality; or if
        // there are extra startCondition / endCondition, some filters are not equality
        // 2. Handle equality filters and one other comparison operator (<, <=, >, >=), see comments below
        if (ispn.getLookupType() != IndexLookupType.EQ &&
                Math.abs(ispn.getSearchKeyExpressions().size() - ExpressionUtil.uncombine(ispn.getEndExpression()).size()) > 1) {
            return plan;
        }

        // exprs will be used as filterExprs to check the index
        // For forward scan, the initial value is endExprs and might be changed in different values in variant cases
        // For reverse scan, the initial value is initialExprs which is the "old" endExprs
        List<AbstractExpression> exprs;
        int numOfSearchKeys = ispn.getSearchKeyExpressions().size();
        if (ispn.getLookupType() == IndexLookupType.LT || ispn.getLookupType() == IndexLookupType.LTE) {
            exprs = ExpressionUtil.uncombine(ispn.getInitialExpression());
            numOfSearchKeys -= 1;
        } else {
            exprs = ExpressionUtil.uncombine(ispn.getEndExpression());
        }
        int numberOfExprs = exprs.size();

        // If there is only 1 difference between searchkeyExprs and endExprs,
        // 1. trivial filters can be discarded, 2 possibilities:
        //      a. SELECT MIN(X) FROM T WHERE [other prefix filters] X < / <= ?
        //         <=> SELECT MIN(X) FROM T WHERE [other prefix filters] && the X < / <= ? filter
        //      b. SELECT MAX(X) FROM T WHERE X > / >= ?
        //         <=> SELECT MAX(X) FROM T with post-filter
        // 2. filter should act as equality filter, 2 possibilities
        //      SELECT MIN(X) FROM T WHERE [other prefix filters] X > / >= ?
        //      SELECT MAX(X) FROM T WHERE [other prefix filters] X < / <= ?

        // check if there is other filters for SELECT MAX(X) FROM T WHERE [other prefix filter AND ] X > / >= ?
        // but we should allow SELECT MAX(X) FROM T WHERE X = ?
        if (sortDirection == SortDirectionType.DESC && ispn.getSortDirection() == SortDirectionType.INVALID) {
            if (numberOfExprs > 1 ||
                    (numberOfExprs == 1 && aggExpr.bindingToIndexedExpression(exprs.get(0).getLeft()) == null)) {
                return plan;
            }
        }

        // have an upper bound: # of endingExpr is more than # of searchExpr
        if (numberOfExprs > numOfSearchKeys) {
            // check last ending condition, see whether it is
            //      SELECT MIN(X) FROM T WHERE [other prefix filters] X < / <= ? or
            // other filters will be checked later
            AbstractExpression lastEndExpr = exprs.get(numberOfExprs - 1);
            if ((lastEndExpr.getExpressionType() == ExpressionType.COMPARE_LESSTHAN ||
                 lastEndExpr.getExpressionType() == ExpressionType.COMPARE_LESSTHANOREQUALTO)
                    && lastEndExpr.getLeft().equals(aggExpr)) {
                exprs.remove(lastEndExpr);
            }
        }

        // do not aggressively evaluate all indexes, just examine the index currently in use;
        // because for all qualified indexes, one access plan must have been generated already,
        // and we can take advantage of that
        if (!checkIndex(ispn.getCatalogIndex(), aggExpr, exprs, ispn.getBindings(), ispn.getTargetTableAlias())) {
            return plan;
        } else {
            // we know which end we want to fetch, set the sort direction
            ispn.setSortDirection(sortDirection);

            // for SELECT MIN(X) FROM T WHERE [prefix filters] = ?
            if (numberOfExprs == numOfSearchKeys && sortDirection == SortDirectionType.ASC) {
                if (ispn.getLookupType() == IndexLookupType.GTE) {
                    assert(aggplan.isTableMin());
                    ispn.setSkipNullPredicate(numOfSearchKeys);
                }
            }

            // for SELECT MIN(X) FROM T WHERE [...] X < / <= ?
            // reset the IndexLookupType, remove "added" searchKey, add back to endExpression, and clear "added" predicate
            if (sortDirection == SortDirectionType.ASC &&
                    (ispn.getLookupType() == IndexLookupType.LT || ispn.getLookupType() == IndexLookupType.LTE)){
                ispn.setLookupType(IndexLookupType.GTE);
                ispn.removeLastSearchKey();
                ispn.addEndExpression(ExpressionUtil.uncombine(ispn.getInitialExpression()).get(numberOfExprs - 1));
                ispn.resetPredicate();
            }
            // add an inline LIMIT plan node to this index scan plan node
            LimitPlanNode lpn = new LimitPlanNode();
            lpn.setLimit(1);
            lpn.setOffset(0);
            ispn.addInlinePlanNode(lpn);

            // ENG-1565: For SELECT MAX(X) FROM T WHERE X > / >= ?, turn the pre-filter to post filter.
            // There are two choices:
            // AggregatePlanNode                AggregatePlanNode
            //  |__ IndexScanPlanNode       =>      |__FilterPlanNode
            //                                              |__IndexScanPlanNode with no filter
            //                                                      |__LimitPlanNode
            //                          OR
            // AggregatePlanNode                AggregatePlanNode with filter
            //  |__ IndexScanPlanNode       =>      |__IndexScanPlanNode with no filter
            //                                              |__LimitPlanNode
            // For now, we take the second approach.
            if (sortDirection == SortDirectionType.DESC &&
                    !ispn.getSearchKeyExpressions().isEmpty() &&
                    exprs.isEmpty() &&
                    ExpressionUtil.uncombine(ispn.getInitialExpression()).isEmpty()) {
                AbstractExpression newPredicate = new ComparisonExpression();
                if (ispn.getLookupType() == IndexLookupType.GT)
                    newPredicate.setExpressionType(ExpressionType.COMPARE_GREATERTHAN);
                if (ispn.getLookupType() == IndexLookupType.GTE)
                    newPredicate.setExpressionType(ExpressionType.COMPARE_GREATERTHANOREQUALTO);
                newPredicate.setRight(ispn.getSearchKeyExpressions().get(0));
                newPredicate.setLeft(aggExpr);
                newPredicate.setValueType(aggExpr.getValueType());
                ispn.clearSearchKeyExpression();
                aggplan.setPrePredicate(newPredicate);
            }

            return plan;
        }
    }

    private Index findQualifiedIndex(SeqScanPlanNode seqScan, AbstractExpression aggExpr, List<AbstractExpression> bindingExprs) {
        String tableName = seqScan.getTargetTableName();
        CatalogMap<Index> allIndexes = m_parsedStmt.m_db.getTables().get(tableName).getIndexes();

        String fromTableAlias = seqScan.getTargetTableAlias();

        for (Index index : allIndexes) {
            if (checkIndex(index, aggExpr, new ArrayList<AbstractExpression>(), bindingExprs, fromTableAlias)) {
                return index;
            }
        }
        return null;
    }

    private boolean checkIndex(Index index, AbstractExpression aggExpr,
            List<AbstractExpression> filterExprs, List<AbstractExpression> bindingExprs, String fromTableAlias) {

        if (!IndexType.isScannable(index.getType())) {
            return false;
        }

        String exprsjson = index.getExpressionsjson();

        if (exprsjson.isEmpty()) {
            // if the index is on simple columns, aggregate expression must be a simple column too
            if (aggExpr.getExpressionType() != ExpressionType.VALUE_TUPLE) {
                return false;
            }

            return checkPureColumnIndex(index, ((TupleValueExpression)aggExpr).getColumnIndex(), filterExprs);

        } else {
            // either pure expression index or mix of expressions and simple columns
            List<AbstractExpression> indexedExprs = null;
            StmtTableScan tableScan = m_parsedStmt.m_tableAliasMap.get(fromTableAlias);

            try {
                indexedExprs = AbstractExpression.fromJSONArrayString(exprsjson, tableScan);
            } catch (JSONException e) {
                e.printStackTrace();
                assert(false);
                return false;
            }

            return checkExpressionIndex(indexedExprs, aggExpr, filterExprs, bindingExprs);
        }
    }

    // lookup aggCol up to min((filterSize + 1), indexedColIdx.size())
    // aggCol can be one of equality comparison key (then a constant value),
    // or all filters compose the complete set of prefix key components
    private static boolean checkPureColumnIndex(Index index, int aggCol, List<AbstractExpression> filterExprs) {

        boolean found = false;

        // all left child of filterExprs must be of type TupleValueExpression in equality comparison
        for (AbstractExpression expr : filterExprs) {
            if (expr.getExpressionType() != ExpressionType.COMPARE_EQUAL) {
                return false;
            }
            if (!(expr.getLeft() instanceof TupleValueExpression)) {
                return false;
            }
            if (((TupleValueExpression)expr.getLeft()).getColumnIndex() == aggCol) {
                found = true;
            }
        }

        if (found) {
            return true;
        }
        if (index.getColumns().size() > filterExprs.size()) {
            List<ColumnRef> indexedColRefs = CatalogUtil.getSortedCatalogItems(index.getColumns(), "index");

            if (indexedColRefs.get(filterExprs.size()).getColumn().getIndex() == aggCol) {
                return true;
            }
        }

        return false;
    }

    private static boolean checkExpressionIndex(List<AbstractExpression> indexedExprs,
            AbstractExpression aggExpr,
            List<AbstractExpression> filterExprs,
            List<AbstractExpression> bindingExprs) {

        List<AbstractExpression> newBindings = null;

        // check type of every filters
        for (AbstractExpression expr : filterExprs) {
            if (expr.getExpressionType() != ExpressionType.COMPARE_EQUAL) {
                return false;
            }
        }

        // first check the indexExpr which is the immediate next one after filterExprs
        if (indexedExprs.size() > filterExprs.size()) {
            newBindings = aggExpr.bindingToIndexedExpression(indexedExprs.get(filterExprs.size()));
            if (newBindings != null) {
                bindingExprs.addAll(newBindings);
                return true;
            }
        }

        // indexedExprs.size() == filterExprs.size()
        // bind aggExpr with indexedExprs
        // add the binding and return when found one (must be in filter as well)
        for (AbstractExpression expr : indexedExprs) {
            newBindings = aggExpr.bindingToIndexedExpression(expr);
            if (newBindings != null) {
                bindingExprs.addAll(newBindings);
                return true;
            }
        }

        return false;
    }

}
TOP

Related Classes of org.voltdb.planner.microoptimizations.ReplaceWithIndexLimit

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.