/* This file is part of VoltDB. * Copyright (C) 2008-2017 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.parseinfo; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Set; import org.voltdb.expressions.AbstractExpression; import org.voltdb.expressions.ConstantValueExpression; import org.voltdb.expressions.ExpressionUtil; import org.voltdb.types.JoinType; /** * A BranchNode is an interior node of a join tree. */ public class BranchNode extends JoinNode { // Join type private JoinType m_joinType; // Left child private JoinNode m_leftNode = null; // Right child private JoinNode m_rightNode = null; // index into the query catalog cache for a table alias /** * Construct a join node * @param id - node unique id * @param joinType - join type * @param leftNode - left node * @param rightNode - right node */ public BranchNode(int id, JoinType joinType, JoinNode leftNode, JoinNode rightNode) { super(id); m_joinType = joinType; m_leftNode = leftNode; m_rightNode = rightNode; updateContentDeterminismMessage(leftNode.getContentDeterminismMessage()); updateContentDeterminismMessage(rightNode.getContentDeterminismMessage()); } /** * Deep clone */ @Override public Object clone() { assert(m_leftNode != null && m_rightNode != null); JoinNode leftNode = (JoinNode) m_leftNode.clone(); JoinNode rightNode = (JoinNode) m_rightNode.clone(); BranchNode newNode = new BranchNode(m_id, m_joinType, leftNode, rightNode); if (m_joinExpr != null) { newNode.m_joinExpr = m_joinExpr.clone(); } if (m_whereExpr != null) { newNode.m_whereExpr = m_whereExpr.clone(); } return newNode; } public void setJoinType(JoinType joinType) { m_joinType = joinType; } public JoinType getJoinType() { return m_joinType; } @Override public JoinNode getLeftNode() { assert(m_leftNode != null); return m_leftNode; } @Override public JoinNode getRightNode() { assert(m_rightNode != null); return m_rightNode; } @Override public void analyzeJoinExpressions(List<AbstractExpression> noneList) { JoinNode leftChild = getLeftNode(); JoinNode rightChild = getRightNode(); leftChild.analyzeJoinExpressions(noneList); rightChild.analyzeJoinExpressions(noneList); // At this moment all RIGHT joins are already converted to the LEFT ones assert (getJoinType() != JoinType.RIGHT); ArrayList<AbstractExpression> joinList = new ArrayList<>(); ArrayList<AbstractExpression> whereList = new ArrayList<>(); // Collect node's own join and where expressions joinList.addAll(ExpressionUtil.uncombineAny(getJoinExpression())); whereList.addAll(ExpressionUtil.uncombineAny(getWhereExpression())); // Collect children expressions only if a child is a leaf. They are not classified yet if ( ! (leftChild instanceof BranchNode)) { joinList.addAll(leftChild.m_joinInnerList); leftChild.m_joinInnerList.clear(); whereList.addAll(leftChild.m_whereInnerList); leftChild.m_whereInnerList.clear(); } if ( ! (rightChild instanceof BranchNode)) { joinList.addAll(rightChild.m_joinInnerList); rightChild.m_joinInnerList.clear(); whereList.addAll(rightChild.m_whereInnerList); rightChild.m_whereInnerList.clear(); } Collection<String> outerTables = leftChild.generateTableJoinOrder(); Collection<String> innerTables = rightChild.generateTableJoinOrder(); // Classify join expressions into the following categories: // 1. The OUTER-only join conditions. If any are false for a given outer tuple, // then NO inner tuples should match it (and it can automatically get null-padded by the join // without even considering the inner table). Testing the outer-only conditions // COULD be considered as an optimal first step to processing each outer tuple // 2. The INNER-only join conditions apply to the inner tuples (even prior to considering any outer tuple). // if true for a given inner tuple, the condition has no effect, // if false, it prevents the inner tuple from matching ANY outer tuple, // In case of multi-tables join, they could be pushed down to a child node if this node is a join itself // 3. The two-sided expressions that get evaluated on each combination of outer and inner tuple // and either accept or reject that particular combination. // 4. The TVE expressions where neither inner nor outer tables are involved. This is not possible // for the currently supported two table joins but could change if number of tables > 2. // Constant Value Expression may fall into this category. classifyJoinExpressions(joinList, outerTables, innerTables, m_joinOuterList, m_joinInnerList, m_joinInnerOuterList, noneList); // Apply implied transitive constant filter to join expressions // outer.partkey = ? and outer.partkey = inner.partkey is equivalent to // outer.partkey = ? and inner.partkey = ? applyTransitiveEquivalence(m_joinOuterList, m_joinInnerList, m_joinInnerOuterList); // Classify where expressions into the following categories: // 1. The OUTER-only filter conditions. If any are false for a given outer tuple, // nothing in the join processing of that outer tuple will get it past this filter, // so it makes sense to "push this filter down" to pre-qualify the outer tuples before they enter the join. // 2. The INNER-only join conditions. If these conditions reject NULL inner tuple it make sense to // move them "up" to the join conditions, otherwise they must remain post-join conditions // to preserve outer join semantic // 3. The two-sided expressions. Same as the inner only conditions. // 4. The TVE expressions where neither inner nor outer tables are involved. Same as for the join expressions classifyJoinExpressions(whereList, outerTables, innerTables, m_whereOuterList, m_whereInnerList, m_whereInnerOuterList, noneList); // Apply implied transitive constant filter to where expressions applyTransitiveEquivalence(m_whereOuterList, m_whereInnerList, m_whereInnerOuterList); // In case of multi-table joins certain expressions could be pushed down to the children // to improve join performance. pushDownExpressions(noneList); Iterator<AbstractExpression> iter = noneList.iterator(); while (iter.hasNext()) { AbstractExpression noneExpr = iter.next(); // Allow only CVE(TRUE/FALSE) for now. // Though it does seem strange to be adding a constant TRUE or FALSE // to a list of conjunctions rather than replacing it with an empty // list or a single FALSE element. // TODO: there may be other use cases that can be handled the same way // as CVEs like predicates based on non-correlated subqueries or predicates // based on correlation parameters from parent queries. These would require // additional testing to be enabled here. // TODO: it seems like there are at least some cases that would perform // better with these predicates pushed down to the inner child node. if (noneExpr instanceof ConstantValueExpression) { m_whereInnerOuterList.add(noneExpr); iter.remove(); } } } /** * Push down each WHERE expression on a given join node to the most specific child join * or table the expression applies to. * 1. The OUTER WHERE expressions can be pushed down to the outer (left) child for all joins * (INNER and LEFT). * 2. The INNER WHERE expressions can be pushed down to the inner (right) child for the INNER joins. * 3. The WHERE expressions must be preserved for the FULL join type. * @param joinNode JoinNode */ protected void pushDownExpressions(List<AbstractExpression> noneList) { JoinType joinType = getJoinType(); if (joinType == JoinType.FULL) { return; } JoinNode outerNode = getLeftNode(); if (outerNode instanceof BranchNode) { ((BranchNode)outerNode).pushDownExpressionsRecursively(m_whereOuterList, noneList); } JoinNode innerNode = getRightNode(); if (innerNode instanceof BranchNode && joinType == JoinType.INNER) { ((BranchNode)innerNode).pushDownExpressionsRecursively(m_whereInnerList, noneList); } } private void pushDownExpressionsRecursively(List<AbstractExpression> pushDownExprList, List<AbstractExpression> noneList) { // It is a join node. Classify pushed down expressions as inner, outer, or inner-outer // WHERE expressions. Collection<String> outerTables = getLeftNode().generateTableJoinOrder(); Collection<String> innerTables = getRightNode().generateTableJoinOrder(); classifyJoinExpressions(pushDownExprList, outerTables, innerTables, m_whereOuterList, m_whereInnerList, m_whereInnerOuterList, noneList); // Remove them from the original list pushDownExprList.clear(); // Descend to the inner child pushDownExpressions(noneList); } @Override public void explain_recurse(StringBuilder sb, String indent) { // Node id. Must be unique within a given tree sb.append(indent).append("JOIN NODE id: " + m_id).append("\n"); // Join expression associated with this node if (m_joinExpr != null) { sb.append(indent).append(m_joinExpr.explain("be explicit")).append("\n"); } // Additional filter expression (WHERE) associated with this node if (m_whereExpr != null) { sb.append(indent).append(m_whereExpr.explain("be explicit")).append("\n"); } // Join type sb.append(indent).append("join type: ").append(m_joinType.name()).append("\n"); // Buckets for children expression classification explain_filter_list(sb, indent, "join outer:", m_joinOuterList); explain_filter_list(sb, indent, "join inner:", m_joinInnerList); explain_filter_list(sb, indent, "join inner outer:", m_joinInnerOuterList); explain_filter_list(sb, indent, "where outer:", m_whereOuterList); explain_filter_list(sb, indent, "where inner:", m_whereInnerList); explain_filter_list(sb, indent, "where inner outer:", m_whereInnerOuterList); String extraIndent = " "; if (m_leftNode != null) { m_leftNode.explain_recurse(sb, indent + extraIndent); } if (m_rightNode != null) { m_rightNode.explain_recurse(sb, indent + extraIndent); } } @Override protected void collectEquivalenceFilters( HashMap<AbstractExpression, Set<AbstractExpression>> equivalenceSet, ArrayDeque<JoinNode> joinNodes) { //* enable to debug */ System.out.println("DEBUG: Branch cEF in " + this + " nodes:" + joinNodes.size() + " filters:" + equivalenceSet.size()); if ( ! m_whereInnerList.isEmpty()) { ExpressionUtil.collectPartitioningFilters(m_whereInnerList, equivalenceSet); } if ( ! m_whereOuterList.isEmpty()) { ExpressionUtil.collectPartitioningFilters(m_whereOuterList, equivalenceSet); } if ( ! m_whereInnerOuterList.isEmpty()) { ExpressionUtil.collectPartitioningFilters(m_whereInnerOuterList, equivalenceSet); } if ( ! m_joinInnerOuterList.isEmpty()) { ExpressionUtil.collectPartitioningFilters(m_joinInnerOuterList, equivalenceSet); } // One-sided join criteria can not be used to infer single partitioining for a // non-inner query. In general, they do not prevent results from being generated // on the partitions that don't have partition-key-qualified rows. if (m_joinType == JoinType.INNER) { if ( ! m_joinInnerList.isEmpty()) { ExpressionUtil.collectPartitioningFilters(m_joinInnerList, equivalenceSet); } if ( ! m_joinOuterList.isEmpty()) { ExpressionUtil.collectPartitioningFilters(m_joinOuterList, equivalenceSet); } } if (m_leftNode != null) { joinNodes.add(m_leftNode); } if (m_rightNode != null) { joinNodes.add(m_rightNode); } //* enable to debug */ System.out.println("DEBUG: Branch cEF out " + this + " nodes:" + joinNodes.size() + " filters:" + equivalenceSet.size()); } /** * Transform all RIGHT joins from the tree into the LEFT ones by swapping the nodes and their join types */ public void toLeftJoin() { assert((m_leftNode != null && m_rightNode != null) || (m_leftNode == null && m_rightNode == null)); if (m_leftNode == null && m_rightNode == null) { // End of recursion return; } // recursive calls if (m_leftNode instanceof BranchNode) { ((BranchNode)m_leftNode).toLeftJoin(); } if (m_rightNode instanceof BranchNode) { ((BranchNode)m_rightNode).toLeftJoin(); } // Swap own children if (m_joinType == JoinType.RIGHT) { JoinNode node = m_rightNode; m_rightNode = m_leftNode; m_leftNode = node; m_joinType = JoinType.LEFT; } } /** * Starting from the root recurse to its children stopping at the first join node * of the different type and discontinue the tree at this point by replacing the join node with * the temporary node which id matches the join node id. This join node is the root of the next * sub-tree. * @param root - The root of the join tree * @param leafNodes - the list of the root nodes of the next sub-trees */ @Override protected void extractSubTree(List<JoinNode> leafNodes) { JoinNode[] children = {m_leftNode, m_rightNode}; for (JoinNode child : children) { // Leaf nodes don't have a significant join type, // test for them first and never attempt to start a new tree at a leaf. if ( ! (child instanceof BranchNode)) { continue; } if (((BranchNode)child).m_joinType == m_joinType) { // The join type for this node is the same as the root's one // Keep walking down the tree child.extractSubTree(leafNodes); } else { // The join type for this join differs from the root's one // Terminate the sub-tree leafNodes.add(child); // Replace the join node with the temporary node having the id negated JoinNode tempNode = new TableLeafNode( -child.m_id, child.m_joinExpr, child.m_whereExpr, null); if (child == m_leftNode) { m_leftNode = tempNode; } else { m_rightNode = tempNode; } } } } /** * Returns true if one of the tree nodes has outer join */ @Override public boolean hasOuterJoin() { assert(m_leftNode != null && m_rightNode != null); return m_joinType != JoinType.INNER || m_leftNode.hasOuterJoin() || m_rightNode.hasOuterJoin(); } /** * Returns a list of immediate sub-queries which are part of this query. * @return List<AbstractParsedStmt> - list of sub-queries from this query */ @Override public void extractSubQueries(List<StmtSubqueryScan> subQueries) { if (m_leftNode != null) { m_leftNode.extractSubQueries(subQueries); } if (m_rightNode != null) { m_rightNode.extractSubQueries(subQueries); } } @Override protected void listNodesJoinOrderRecursive(ArrayList<JoinNode> nodes, boolean appendBranchNodes) { m_leftNode.listNodesJoinOrderRecursive(nodes, appendBranchNodes); m_rightNode.listNodesJoinOrderRecursive(nodes, appendBranchNodes); if (appendBranchNodes) { nodes.add(this); } } @Override protected void queueChildren(ArrayDeque<JoinNode> joinNodes) { joinNodes.add(m_leftNode); joinNodes.add(m_rightNode); } @Override protected boolean replaceChild(JoinNode node) { // can't replace self assert (Math.abs(m_id) != Math.abs(node.m_id)); if (Math.abs(m_leftNode.m_id) == Math.abs(node.m_id)) { m_leftNode = node; return true; } if (Math.abs(m_rightNode.m_id) == Math.abs(node.m_id)) { m_rightNode = node; return true; } if (m_leftNode.replaceChild(node)) { return true; } if (m_rightNode.replaceChild(node)) { return true; } return false; } /** * Returns if all the join operations within this join tree are inner joins. * @return true or false. */ @Override public boolean allInnerJoins() { return m_joinType == JoinType.INNER && (m_leftNode == null || m_leftNode.allInnerJoins()) && (m_rightNode == null || m_rightNode.allInnerJoins()); } @Override public void gatherJoinExpressions(List<AbstractExpression> checkExpressions) { super.gatherJoinExpressions(checkExpressions); m_leftNode.gatherJoinExpressions(checkExpressions); m_rightNode.gatherJoinExpressions(checkExpressions); } }