/** * Copyright (C) 2009-2013 FoundationDB, LLC * * 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.sql.optimizer.rule; import com.foundationdb.server.error.CorruptedPlanException; import com.foundationdb.sql.optimizer.plan.*; import com.foundationdb.sql.optimizer.plan.JoinNode.JoinType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** Convert nested loop join into map. * This rule only does the immediate conversion to a Map. The map * still needs to be folded after conditions have moved down and so on. */ public class NestedLoopMapper extends BaseRule { private static final Logger logger = LoggerFactory.getLogger(NestedLoopMapper.class); @Override protected Logger getLogger() { return logger; } static class NestedLoopsJoinsFinder implements PlanVisitor, ExpressionVisitor { List<JoinNode> result = new ArrayList<>(); public List<JoinNode> find(PlanNode root) { root.accept(this); return result; } @Override public boolean visitEnter(PlanNode n) { return visit(n); } @Override public boolean visitLeave(PlanNode n) { return true; } @Override public boolean visit(PlanNode n) { if (n instanceof JoinNode) { JoinNode j = (JoinNode)n; switch (j.getImplementation()) { case NESTED_LOOPS: case BLOOM_FILTER: case HASH_TABLE: result.add(j); } } return true; } @Override public boolean visitEnter(ExpressionNode n) { return visit(n); } @Override public boolean visitLeave(ExpressionNode n) { return true; } @Override public boolean visit(ExpressionNode n) { return true; } } @Override public void apply(PlanContext planContext) { BaseQuery query = (BaseQuery)planContext.getPlan(); List<JoinNode> joins = new NestedLoopsJoinsFinder().find(query); for (JoinNode join : joins) { PlanNode outer = join.getLeft(); PlanNode inner = join.getRight(); if (join.hasJoinConditions()) { if (join.getJoinType().isInner() || join.getJoinType().isSemi()) { outer = moveConditionsToOuterNode(outer, join.getJoinConditions(), getQuery(join).getOuterTables()); } if (join.hasJoinConditions()) inner = new Select(inner, join.getJoinConditions()); } PlanNode map; switch (join.getImplementation()) { case NESTED_LOOPS: map = new MapJoin(join.getJoinType(), outer, inner); break; case BLOOM_FILTER: { HashJoinNode hjoin = (HashJoinNode)join; BloomFilter bf = (BloomFilter)hjoin.getHashTable(); map = new BloomFilterFilter(bf, hjoin.getMatchColumns(), outer, inner); PlanNode loader = hjoin.getLoader(); loader = new Project(loader, hjoin.getHashColumns()); map = new UsingBloomFilter(bf, loader, map); } break; case HASH_TABLE: { map = new MapJoin(join.getJoinType(), outer, inner); HashJoinNode hjoin = (HashJoinNode)join; HashTable ht = (HashTable)hjoin.getHashTable(); PlanNode loader = hjoin.getLoader(); map = new UsingHashTable(ht, loader, map, hjoin.getHashColumns(), hjoin.getTKeyComparables(), hjoin.getCollators()); } break; default: assert false : join; map = join; } join.getOutput().replaceInput(join, map); } } private BaseQuery getQuery(PlanNode node) { PlanWithInput output = node.getOutput(); while (output != null) { if (output instanceof BaseQuery) { return (BaseQuery) output; } output = output.getOutput(); } throw new CorruptedPlanException("PlanNode did not have BaseQuery"); } private PlanNode moveConditionsToOuterNode(PlanNode planNode, ConditionList conditions, Set<ColumnSource> outerSources) { ConditionList selectConditions = new ConditionList(); Iterator<ConditionExpression> iterator = conditions.iterator(); while (iterator.hasNext()) { ConditionExpression condition = iterator.next(); Set<ColumnSource> columnSources = new ConditionColumnSourcesFinder().find(condition); columnSources.removeAll(outerSources); PlanNodeProvidesSourcesChecker checker = new PlanNodeProvidesSourcesChecker(columnSources, planNode); if (checker.run()) { selectConditions.add(condition); iterator.remove(); } } return selectConditions.isEmpty() ? planNode : new Select(planNode, selectConditions); } private static class PlanNodeProvidesSourcesChecker implements PlanVisitor { private final Set<ColumnSource> columnSources; private final PlanNode planNode; private PlanNodeProvidesSourcesChecker(Set<ColumnSource> columnSources, PlanNode node) { this.columnSources = columnSources; this.planNode = node; } public boolean run() { planNode.accept(this); return columnSources.isEmpty(); } @Override public boolean visitEnter(PlanNode n) { if (columnSources.isEmpty()) { return false; } if (n instanceof ColumnSource) { columnSources.remove(n); // We want to go inside, because if you have a Group Join, the inner groups are nested nodes within // the outer table source return true; } if (n instanceof Subquery) { // subquery sources are the source you can see from outside, don't go into the inner subquery return false; } return true; } @Override public boolean visitLeave(PlanNode n) { return false; } @Override public boolean visit(PlanNode n) { if (columnSources.isEmpty()) { return false; } if (n instanceof ColumnSource) { columnSources.remove(n); return true; } if (n instanceof Subquery) { return false; } return true; } } }