/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.query.optimizer.relational.rules; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.teiid.api.exception.query.QueryMetadataException; import org.teiid.api.exception.query.QueryPlannerException; import org.teiid.core.TeiidComponentException; import org.teiid.query.analysis.AnalysisRecord; import org.teiid.query.metadata.QueryMetadataInterface; import org.teiid.query.optimizer.capabilities.CapabilitiesFinder; import org.teiid.query.optimizer.relational.OptimizerRule; import org.teiid.query.optimizer.relational.RuleStack; import org.teiid.query.optimizer.relational.plantree.NodeConstants; import org.teiid.query.optimizer.relational.plantree.NodeEditor; import org.teiid.query.optimizer.relational.plantree.PlanNode; import org.teiid.query.sql.LanguageObject; import org.teiid.query.sql.lang.Criteria; import org.teiid.query.sql.lang.JoinType; import org.teiid.query.sql.symbol.AggregateSymbol; import org.teiid.query.sql.symbol.ElementSymbol; import org.teiid.query.sql.symbol.Expression; import org.teiid.query.sql.symbol.GroupSymbol; import org.teiid.query.sql.visitor.FunctionCollectorVisitor; import org.teiid.query.sql.visitor.GroupsUsedByElementsVisitor; import org.teiid.query.sql.visitor.ValueIteratorProviderCollectorVisitor; import org.teiid.query.util.CommandContext; /** * Removes optional join nodes if elements originating from that join are not used in the * top level project symbols. */ public class RuleRemoveOptionalJoins implements OptimizerRule { public PlanNode execute(PlanNode plan, QueryMetadataInterface metadata, CapabilitiesFinder capFinder, RuleStack rules, AnalysisRecord analysisRecord, CommandContext context) throws QueryPlannerException, QueryMetadataException, TeiidComponentException { List<PlanNode> projectNodes = NodeEditor.findAllNodes(plan, NodeConstants.Types.PROJECT); HashSet<PlanNode> skipNodes = new HashSet<PlanNode>(); for (PlanNode projectNode : projectNodes) { if (projectNode.getChildCount() == 0 || projectNode.getProperty(NodeConstants.Info.INTO_GROUP) != null) { continue; } PlanNode groupNode = NodeEditor.findNodePreOrder(projectNode, NodeConstants.Types.GROUP, NodeConstants.Types.SOURCE | NodeConstants.Types.JOIN); if (groupNode != null) { projectNode = groupNode; } Set<GroupSymbol> requiredForOptional = getRequiredGroupSymbols(projectNode.getFirstChild()); boolean done = false; while (!done) { done = true; List<PlanNode> joinNodes = NodeEditor.findAllNodes(projectNode, NodeConstants.Types.JOIN, NodeConstants.Types.SOURCE); for (PlanNode planNode : joinNodes) { if (skipNodes.contains(planNode)) { continue; } if (!planNode.getExportedCorrelatedReferences().isEmpty()) { skipNodes.add(planNode); continue; } Set<GroupSymbol> required = getRequiredGroupSymbols(planNode); List<PlanNode> removed = removeJoin(required, requiredForOptional, planNode, planNode.getFirstChild(), analysisRecord, metadata); if (removed != null) { skipNodes.addAll(removed); done = false; continue; } removed = removeJoin(required, requiredForOptional, planNode, planNode.getLastChild(), analysisRecord, metadata); if (removed != null) { skipNodes.addAll(removed); done = false; } } } } return plan; } private Set<GroupSymbol> getRequiredGroupSymbols(PlanNode planNode) { return GroupsUsedByElementsVisitor.getGroups((Collection<? extends LanguageObject>)planNode.getProperty(NodeConstants.Info.OUTPUT_COLS)); } /** * remove the optional node if possible * @throws QueryPlannerException * @throws TeiidComponentException * @throws QueryMetadataException */ private List<PlanNode> removeJoin(Set<GroupSymbol> required, Set<GroupSymbol> requiredForOptional, PlanNode joinNode, PlanNode optionalNode, AnalysisRecord record, QueryMetadataInterface metadata) throws QueryPlannerException, QueryMetadataException, TeiidComponentException { boolean correctFrame = false; boolean isOptional = optionalNode.hasBooleanProperty(NodeConstants.Info.IS_OPTIONAL); if (isOptional) { required = requiredForOptional; correctFrame = true; } if (!Collections.disjoint(optionalNode.getGroups(), required)) { return null; } if (isOptional) { //prevent bridge table removal HashSet<GroupSymbol> joinGroups = new HashSet<GroupSymbol>(); PlanNode parentNode = joinNode; while (parentNode.getType() != NodeConstants.Types.PROJECT) { PlanNode current = parentNode; parentNode = parentNode.getParent(); if (current.getType() != NodeConstants.Types.SELECT && current.getType() != NodeConstants.Types.JOIN) { continue; } Set<GroupSymbol> currentGroups = current.getGroups(); if (current.getType() == NodeConstants.Types.JOIN) { currentGroups = GroupsUsedByElementsVisitor.getGroups((List<Criteria>)current.getProperty(NodeConstants.Info.JOIN_CRITERIA)); } if (!Collections.disjoint(currentGroups, optionalNode.getGroups()) && !optionalNode.getGroups().containsAll(currentGroups)) { //we're performing a join boolean wasEmpty = joinGroups.isEmpty(); boolean modified = joinGroups.addAll(current.getGroups()); if (!wasEmpty && modified) { return null; } } } } JoinType jt = (JoinType)joinNode.getProperty(NodeConstants.Info.JOIN_TYPE); boolean usesKey = false; boolean isRight = optionalNode == joinNode.getLastChild(); if (!isOptional && (jt == JoinType.JOIN_INNER || (jt == JoinType.JOIN_LEFT_OUTER && isRight))) { usesKey = isOptionalUsingKey(joinNode, optionalNode, metadata, isRight); } if (!isOptional && !usesKey && (jt != JoinType.JOIN_LEFT_OUTER || !isRight || useNonDistinctRows(joinNode.getParent()))) { return null; } // remove the parent node and move the sibling node upward PlanNode parentNode = joinNode.getParent(); joinNode.removeChild(optionalNode); joinNode.getFirstChild().setProperty(NodeConstants.Info.OUTPUT_COLS, joinNode.getProperty(NodeConstants.Info.OUTPUT_COLS)); NodeEditor.removeChildNode(parentNode, joinNode); joinNode.recordDebugAnnotation((isOptional?"node was marked as optional ":"node will not affect the results"), null, "Removing join node", record, null); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ while (parentNode.getType() != NodeConstants.Types.PROJECT) { PlanNode current = parentNode; parentNode = parentNode.getParent(); if (correctFrame) { if (current.getType() == NodeConstants.Types.SELECT) { if (!Collections.disjoint(current.getGroups(), optionalNode.getGroups())) { current.getFirstChild().setProperty(NodeConstants.Info.OUTPUT_COLS, current.getProperty(NodeConstants.Info.OUTPUT_COLS)); NodeEditor.removeChildNode(parentNode, current); } } else if (current.getType() == NodeConstants.Types.JOIN) { if (!Collections.disjoint(current.getGroups(), optionalNode.getGroups())) { List<Criteria> crits = (List<Criteria>) current.getProperty(NodeConstants.Info.JOIN_CRITERIA); if (crits != null && !crits.isEmpty()) { for (Iterator<Criteria> iterator = crits.iterator(); iterator.hasNext();) { Criteria criteria = iterator.next(); if (!Collections.disjoint(GroupsUsedByElementsVisitor.getGroups(criteria), optionalNode.getGroups())) { iterator.remove(); } } if (crits.isEmpty()) { JoinType joinType = (JoinType) current.getProperty(NodeConstants.Info.JOIN_TYPE); if (joinType == JoinType.JOIN_INNER) { current.setProperty(NodeConstants.Info.JOIN_TYPE, JoinType.JOIN_CROSS); } } } } } } else if (current.getType() != NodeConstants.Types.JOIN) { break; } if (current.getType() == NodeConstants.Types.JOIN) { current.getGroups().removeAll(optionalNode.getGroups()); } } return NodeEditor.findAllNodes(optionalNode, NodeConstants.Types.JOIN); } private boolean isOptionalUsingKey(PlanNode joinNode, PlanNode optionalNode, QueryMetadataInterface metadata, boolean isRight) throws TeiidComponentException, QueryMetadataException { //TODO: check if key preserved to allow for more than just a single group as optional //for now we just look for a single group if (optionalNode.getGroups().size() != 1) { return false; } PlanNode left = isRight?joinNode.getFirstChild():joinNode.getLastChild(); LinkedList<Expression> leftExpressions = new LinkedList<Expression>(); LinkedList<Expression> rightExpressions = new LinkedList<Expression>(); LinkedList<Criteria> nonEquiJoinCriteria = new LinkedList<Criteria>(); RuleChooseJoinStrategy.separateCriteria(left.getGroups(), optionalNode.getGroups(), leftExpressions, rightExpressions, (List<Criteria>)joinNode.getProperty(NodeConstants.Info.JOIN_CRITERIA), nonEquiJoinCriteria); if (!nonEquiJoinCriteria.isEmpty()) { for (Criteria crit : nonEquiJoinCriteria) { if (!Collections.disjoint(GroupsUsedByElementsVisitor.getGroups(crit), optionalNode.getGroups())) { return false; //additional predicates still need to be applied, or ignored via hint } } } ArrayList<Object> leftIds = new ArrayList<Object>(leftExpressions.size()); ArrayList<Object> rightIds = new ArrayList<Object>(rightExpressions.size()); for (Expression expr : leftExpressions) { if (expr instanceof ElementSymbol) { leftIds.add(((ElementSymbol) expr).getMetadataID()); } } for (Expression expr : rightExpressions) { if (expr instanceof ElementSymbol) { rightIds.add(((ElementSymbol) expr).getMetadataID()); } else { return false; //only allow a key join } } outer: for (GroupSymbol group : left.getGroups()) { Collection fks = metadata.getForeignKeysInGroup(group.getMetadataID()); for (Object fk : fks) { List fkColumns = metadata.getElementIDsInKey(fk); if (!leftIds.containsAll(fkColumns)) { continue; } Object pk = metadata.getPrimaryKeyIDForForeignKeyID(fk); List pkColumns = metadata.getElementIDsInKey(pk); if ((rightIds.size() != pkColumns.size()) || !rightIds.containsAll(pkColumns)) { continue; } if (!isRight) { //match up to the replacement logic below JoinUtil.swapJoinChildren(joinNode); } return true; } } return false; } static boolean useNonDistinctRows(PlanNode parent) { while (parent != null) { if (parent.hasBooleanProperty(NodeConstants.Info.IS_DUP_REMOVAL)) { return false; } switch (parent.getType()) { case NodeConstants.Types.DUP_REMOVE: { return false; } case NodeConstants.Types.SET_OP: { if (!parent.hasBooleanProperty(NodeConstants.Info.USE_ALL)) { return false; } break; } case NodeConstants.Types.GROUP: { Set<AggregateSymbol> aggs = RulePushAggregates.collectAggregates(parent); return AggregateSymbol.areAggregatesCardinalityDependent(aggs); } case NodeConstants.Types.TUPLE_LIMIT: { if (FrameUtil.isOrderedOrStrictLimit(parent)) { return true; } break; } case NodeConstants.Types.PROJECT: { List<Expression> projectCols = (List<Expression>)parent.getProperty(NodeConstants.Info.PROJECT_COLS); for (Expression ex : projectCols) { if (FunctionCollectorVisitor.isNonDeterministic(ex) || !ValueIteratorProviderCollectorVisitor.getValueIteratorProviders(ex).isEmpty()) { return true; } } break; } } parent = parent.getParent(); } return true; } public String toString() { return "RemoveOptionalJoins"; //$NON-NLS-1$ } }