[ASTERIXDB-3355][COMP] Further refactoring of CBO plan generation code.

Change-Id: Ia070f3a84562a9e1fef2b453a6fbf0b08535dae8
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/18160
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: <murali.krishna@couchbase.com>
Reviewed-by: Vijay Sarathy <vijay.sarathy@couchbase.com>
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinEnum.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinEnum.java
index 4d8ca67..e419eea 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinEnum.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinEnum.java
@@ -766,10 +766,6 @@
         // join nodes have been allocated in the JoinEnum
         // add a dummy Plan Node; we do not want planNode at position 0 to be a valid plan
         PlanNode pn = new PlanNode(0, this);
-        pn.jn = null;
-        pn.jnIndexes[0] = pn.jnIndexes[1] = JoinNode.NO_JN;
-        pn.planIndexes[0] = pn.planIndexes[1] = PlanNode.NO_PLAN;
-        pn.opCost = pn.totalCost = new Cost(0);
         allPlans.add(pn);
 
         boolean noCards = false;
@@ -1078,7 +1074,7 @@
     }
 
     private void dumpContext(StringBuilder sb) {
-        sb.append("\n\nOPT CONTEXT").append('\n');
+        sb.append("\n\nCBO CONTEXT").append('\n');
         sb.append("----------------------------------------\n");
         sb.append("BLOCK SIZE = ").append(getCostMethodsHandle().getBufferCachePageSize()).append('\n');
         sb.append("DOP = ").append(getCostMethodsHandle().getDOP()).append('\n');
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinNode.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinNode.java
index b9f48e6..4543190 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinNode.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/JoinNode.java
@@ -102,8 +102,6 @@
     private int limitVal = -1; // only for single dataset joinNodes.
     private List<Integer> applicableJoinConditions;
     protected ILogicalOperator leafInput;
-    private List<Pair<IAccessMethod, Index>> chosenIndexes;
-    private Map<IAccessMethod, AccessMethodAnalysisContext> analyzedAMs;
     protected Index.SampleIndexDetails idxDetails;
     private List<Triple<Index, Double, AbstractFunctionCallExpression>> IndexCostInfo;
     // The triple above is : Index, selectivity, and the index expression
@@ -238,76 +236,6 @@
         return columnar;
     }
 
-    private boolean nestedLoopsApplicable(ILogicalExpression joinExpr) throws AlgebricksException {
-
-        List<LogicalVariable> usedVarList = new ArrayList<>();
-        joinExpr.getUsedVariables(usedVarList);
-        if (usedVarList.size() != 2) {
-            return false;
-        }
-
-        if (joinExpr.getExpressionTag() != LogicalExpressionTag.FUNCTION_CALL) {
-            return false;
-        }
-
-        LogicalVariable var0 = usedVarList.get(0);
-        LogicalVariable var1 = usedVarList.get(1);
-
-        // Find which joinLeafInput these vars belong to.
-        // go thru the leaf inputs and see where these variables came from
-        ILogicalOperator joinLeafInput0 = joinEnum.findLeafInput(Collections.singletonList(var0));
-        if (joinLeafInput0 == null) {
-            return false; // this should not happen unless an assignment is between two joins.
-        }
-
-        ILogicalOperator joinLeafInput1 = joinEnum.findLeafInput(Collections.singletonList(var1));
-        if (joinLeafInput1 == null) {
-            return false;
-        }
-
-        ILogicalOperator innerLeafInput = this.leafInput;
-
-        // This must equal one of the two joinLeafInputsHashMap found above. check for sanity!!
-        if (innerLeafInput != joinLeafInput1 && innerLeafInput != joinLeafInput0) {
-            return false; // This should not happen. So debug to find out why this happened.
-        }
-
-        if (innerLeafInput == joinLeafInput0) {
-            joinEnum.localJoinOp.getInputs().get(0).setValue(joinLeafInput1);
-        } else {
-            joinEnum.localJoinOp.getInputs().get(0).setValue(joinLeafInput0);
-        }
-
-        joinEnum.localJoinOp.getInputs().get(1).setValue(innerLeafInput);
-
-        // We will always use the first join Op to provide the joinOp input for invoking rewritePre
-        AbstractBinaryJoinOperator joinOp = (AbstractBinaryJoinOperator) joinEnum.localJoinOp;
-        joinOp.getCondition().setValue(joinExpr);
-
-        // Now call the rewritePre code
-        IntroduceJoinAccessMethodRule tmp = new IntroduceJoinAccessMethodRule();
-        boolean retVal = tmp.checkApplicable(new MutableObject<>(joinEnum.localJoinOp), joinEnum.optCtx);
-
-        return retVal;
-    }
-
-    /** one is a subset of two */
-    private boolean subset(int one, int two) {
-        return (one & two) == one;
-    }
-
-    private void findApplicableJoinConditions() {
-        List<JoinCondition> joinConditions = joinEnum.getJoinConditions();
-
-        int i = 0;
-        for (JoinCondition jc : joinConditions) {
-            if (subset(jc.datasetBits, this.datasetBits)) {
-                this.applicableJoinConditions.add(i);
-            }
-            i++;
-        }
-    }
-
     public void setCardsAndSizes(Index.SampleIndexDetails idxDetails, ILogicalOperator leafInput)
             throws AlgebricksException {
 
@@ -420,6 +348,23 @@
         setAvgDocSize(idxDetails.getSourceAvgItemSize());
     }
 
+    /** one is a subset of two */
+    private boolean subset(int one, int two) {
+        return (one & two) == one;
+    }
+
+    private void findApplicableJoinConditions() {
+        List<JoinCondition> joinConditions = joinEnum.getJoinConditions();
+
+        int i = 0;
+        for (JoinCondition jc : joinConditions) {
+            if (subset(jc.datasetBits, this.datasetBits)) {
+                this.applicableJoinConditions.add(i);
+            }
+            i++;
+        }
+    }
+
     private List<Integer> getNewJoinConditionsOnly() {
         List<Integer> newJoinConditions = new ArrayList<>();
         JoinNode leftJn = this.leftJn;
@@ -562,34 +507,18 @@
 
     protected int addSingleDatasetPlans() {
         List<PlanNode> allPlans = joinEnum.allPlans;
-        ICost opCost, totalCost;
-        PlanNode pn, cheapestPlan;
+        ICost opCost;
+        PlanNode pn;
         opCost = joinEnum.getCostMethodsHandle().costFullScan(this);
-        totalCost = opCost;
         boolean forceEnum = level <= joinEnum.cboFullEnumLevel;
         if (this.cheapestPlanIndex == PlanNode.NO_PLAN || opCost.costLT(this.cheapestPlanCost) || forceEnum) {
             // for now just add one plan
-            pn = new PlanNode(allPlans.size(), joinEnum);
-            pn.setJoinNode(this);
-            pn.datasetName = this.datasetNames.get(0);
-            pn.leafInput = this.leafInput;
-            pn.setLeftJoinIndex(this.jnArrayIndex);
-            pn.setRightJoinIndex(JoinNode.NO_JN);
-            pn.setLeftPlanIndex(PlanNode.NO_PLAN); // There ane no plans below this plan.
-            pn.setRightPlanIndex(PlanNode.NO_PLAN); // There ane no plans below this plan.
-            pn.opCost = opCost;
-            pn.scanOp = PlanNode.ScanMethod.TABLE_SCAN;
-            pn.totalCost = totalCost;
-
+            pn = new PlanNode(allPlans.size(), joinEnum, this, datasetNames.get(0), leafInput);
+            pn.setScanMethod(PlanNode.ScanMethod.TABLE_SCAN);
+            pn.setScanCosts(opCost);
+            planIndexesArray.add(pn.allPlansIndex);
             allPlans.add(pn);
-            this.planIndexesArray.add(pn.allPlansIndex);
-            if (!forceEnum) {
-                cheapestPlan = pn;
-            } else {
-                cheapestPlan = findCheapestPlan();
-            }
-            this.cheapestPlanCost = cheapestPlan.totalCost;
-            this.cheapestPlanIndex = cheapestPlan.allPlansIndex;
+            setCheapestPlan(pn, forceEnum);
             return pn.allPlansIndex;
         }
         return PlanNode.NO_PLAN;
@@ -675,7 +604,7 @@
 
     private void buildIndexPlans() {
         List<PlanNode> allPlans = joinEnum.getAllPlans();
-        PlanNode pn, cheapestPlan;
+        PlanNode pn;
         ICost opCost, totalCost;
         List<Triple<Index, Double, AbstractFunctionCallExpression>> mandatoryIndexesInfo = new ArrayList<>();
         List<Triple<Index, Double, AbstractFunctionCallExpression>> optionalIndexesInfo = new ArrayList<>();
@@ -766,30 +695,12 @@
         totalCost = opCost.costAdd(mandatoryIndexesCost); // cost of all the indexes chosen
         boolean forceEnum = mandatoryIndexesInfo.size() > 0 || level <= joinEnum.cboFullEnumLevel;
         if (opCost.costLT(this.cheapestPlanCost) || forceEnum) {
-            pn = new PlanNode(allPlans.size(), joinEnum);
-            pn.setJoinNode(this);
-            pn.setDatasetName(getDatasetNames().get(0));
-            pn.setLeafInput(this.leafInput);
-            pn.setLeftJoinIndex(this.jnArrayIndex);
-            pn.setRightJoinIndex(JoinNode.NO_JN);
-            pn.setLeftPlanIndex(PlanNode.NO_PLAN); // There ane no plans below this plan.
-            pn.setRightPlanIndex(PlanNode.NO_PLAN); // There ane no plans below this plan.
-            pn.setOpCost(totalCost);
-            pn.setScanMethod(PlanNode.ScanMethod.INDEX_SCAN);
-            if (mandatoryIndexesInfo.size() > 0) {
-                pn.indexHint = true;
-                pn.numHintsUsed = 1;
-            }
-            pn.setTotalCost(totalCost);
+            pn = new PlanNode(allPlans.size(), joinEnum, this, datasetNames.get(0), leafInput);
+            pn.setScanAndHintInfo(PlanNode.ScanMethod.INDEX_SCAN, mandatoryIndexesInfo);
+            pn.setScanCosts(totalCost);
+            planIndexesArray.add(pn.allPlansIndex);
             allPlans.add(pn);
-            this.planIndexesArray.add(pn.allPlansIndex);
-            if (!forceEnum) {
-                cheapestPlan = pn;
-            } else {
-                cheapestPlan = findCheapestPlan();
-            }
-            this.cheapestPlanCost = cheapestPlan.totalCost; // in the presence of mandatory indexes, this may not be the cheapest plan! But we have no choice!
-            this.cheapestPlanIndex = cheapestPlan.allPlansIndex;
+            setCheapestPlan(pn, forceEnum);
         }
     }
 
@@ -876,8 +787,6 @@
             firstSelop.getCondition().setValue(andAlltheExprs(selExprs));
             boolean index_access_possible =
                     tmp.checkApplicable(new MutableObject<>(leafInput), joinEnum.optCtx, chosenIndexes, analyzedAMs);
-            this.chosenIndexes = chosenIndexes;
-            this.analyzedAMs = analyzedAMs;
             restoreSelExprs(selExprs, selOpers);
             if (index_access_possible) {
                 costAndChooseIndexPlans(leafInput, analyzedAMs);
@@ -906,31 +815,125 @@
         return false;
     }
 
-    protected int buildHashJoinPlan(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression hashJoinExpr, HashJoinExpressionAnnotation hintHashJoin, boolean outerJoin) {
-        List<PlanNode> allPlans = joinEnum.allPlans;
-        PlanNode pn, cheapestPlan;
-        ICost hjCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;
-        this.leftJn = leftJn;
-        this.rightJn = rightJn;
-
+    private boolean hashJoinApplicable(JoinNode leftJn, boolean outerJoin, ILogicalExpression hashJoinExpr) {
         if (nullExtendingSide(leftJn.datasetBits, outerJoin)) {
-            return PlanNode.NO_PLAN;
+            return false;
         }
 
         if (hashJoinExpr == null || hashJoinExpr == ConstantExpression.TRUE) {
-            return PlanNode.NO_PLAN;
+            return false;
         }
 
         if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_LEFTDEEP) && !leftJn.IsBaseLevelJoinNode()
                 && level > joinEnum.cboFullEnumLevel) {
-            return PlanNode.NO_PLAN;
+            return false;
         }
 
         if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_RIGHTDEEP)
                 && !rightJn.IsBaseLevelJoinNode() && level > joinEnum.cboFullEnumLevel) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean nestedLoopsApplicable(ILogicalExpression joinExpr) throws AlgebricksException {
+
+        List<LogicalVariable> usedVarList = new ArrayList<>();
+        joinExpr.getUsedVariables(usedVarList);
+        if (usedVarList.size() != 2) {
+            return false;
+        }
+
+        if (joinExpr.getExpressionTag() != LogicalExpressionTag.FUNCTION_CALL) {
+            return false;
+        }
+
+        LogicalVariable var0 = usedVarList.get(0);
+        LogicalVariable var1 = usedVarList.get(1);
+
+        // Find which joinLeafInput these vars belong to.
+        // go thru the leaf inputs and see where these variables came from
+        ILogicalOperator joinLeafInput0 = joinEnum.findLeafInput(Collections.singletonList(var0));
+        if (joinLeafInput0 == null) {
+            return false; // this should not happen unless an assignment is between two joins.
+        }
+
+        ILogicalOperator joinLeafInput1 = joinEnum.findLeafInput(Collections.singletonList(var1));
+        if (joinLeafInput1 == null) {
+            return false;
+        }
+
+        ILogicalOperator innerLeafInput = this.leafInput;
+
+        // This must equal one of the two joinLeafInputsHashMap found above. check for sanity!!
+        if (innerLeafInput != joinLeafInput1 && innerLeafInput != joinLeafInput0) {
+            return false; // This should not happen. So debug to find out why this happened.
+        }
+
+        if (innerLeafInput == joinLeafInput0) {
+            joinEnum.localJoinOp.getInputs().get(0).setValue(joinLeafInput1);
+        } else {
+            joinEnum.localJoinOp.getInputs().get(0).setValue(joinLeafInput0);
+        }
+
+        joinEnum.localJoinOp.getInputs().get(1).setValue(innerLeafInput);
+
+        // We will always use the first join Op to provide the joinOp input for invoking rewritePre
+        AbstractBinaryJoinOperator joinOp = (AbstractBinaryJoinOperator) joinEnum.localJoinOp;
+        joinOp.getCondition().setValue(joinExpr);
+
+        // Now call the rewritePre code
+        IntroduceJoinAccessMethodRule tmp = new IntroduceJoinAccessMethodRule();
+        boolean retVal = tmp.checkApplicable(new MutableObject<>(joinEnum.localJoinOp), joinEnum.optCtx);
+
+        return retVal;
+    }
+
+    private boolean NLJoinApplicable(JoinNode leftJn, JoinNode rightJn, boolean outerJoin,
+            ILogicalExpression nestedLoopJoinExpr) throws AlgebricksException {
+        if (nullExtendingSide(leftJn.datasetBits, outerJoin)) {
+            return false;
+        }
+
+        if (rightJn.jnArrayIndex > joinEnum.numberOfTerms) {
+            // right side consists of more than one table
+            return false; // nested loop plan not possible.
+        }
+
+        if (nestedLoopJoinExpr == null || !rightJn.nestedLoopsApplicable(nestedLoopJoinExpr)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean CPJoinApplicable(JoinNode leftJn, boolean outerJoin) {
+        if (!joinEnum.cboCPEnumMode) {
+            return false;
+        }
+
+        if (nullExtendingSide(leftJn.datasetBits, outerJoin)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    protected int buildHashJoinPlan(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression hashJoinExpr,
+            HashJoinExpressionAnnotation hintHashJoin, boolean outerJoin) {
+        List<PlanNode> allPlans = joinEnum.allPlans;
+        JoinNode leftJn = leftPlan.getJoinNode();
+        JoinNode rightJn = rightPlan.getJoinNode();
+        PlanNode pn;
+        ICost hjCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;
+        this.leftJn = leftJn;
+        this.rightJn = rightJn;
+
+        if (!hashJoinApplicable(leftJn, outerJoin, hashJoinExpr)) {
             return PlanNode.NO_PLAN;
         }
+
         boolean forceEnum = hintHashJoin != null || joinEnum.forceJoinOrderMode
                 || !joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_ZIGZAG) || outerJoin
                 || level <= joinEnum.cboFullEnumLevel;
@@ -943,65 +946,31 @@
                     .costAdd(allPlans.get(rightPlan.allPlansIndex).totalCost);
             totalCost = hjCost.costAdd(leftExchangeCost).costAdd(rightExchangeCost).costAdd(childCosts);
             if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost) || forceEnum) {
-                pn = new PlanNode(allPlans.size(), joinEnum);
-                pn.setJoinNode(this);
-                pn.outerJoin = outerJoin;
-                pn.setLeftJoinIndex(leftJn.jnArrayIndex);
-                pn.setRightJoinIndex(rightJn.jnArrayIndex);
-                pn.setLeftPlanIndex(leftPlan.allPlansIndex);
-                pn.setRightPlanIndex(rightPlan.allPlansIndex);
-                pn.joinOp = PlanNode.JoinMethod.HYBRID_HASH_JOIN; // need to check that all the conditions have equality predicates ONLY.
-                pn.joinHint = hintHashJoin;
-                pn.numHintsUsed = allPlans.get(pn.getLeftPlanIndex()).numHintsUsed
-                        + allPlans.get(pn.getRightPlanIndex()).numHintsUsed;
-                if (hintHashJoin != null) {
-                    pn.numHintsUsed++;
-                }
-                pn.side = HashJoinExpressionAnnotation.BuildSide.RIGHT;
-                pn.joinExpr = hashJoinExpr;
-                pn.opCost = hjCost;
-                pn.totalCost = totalCost;
-                pn.leftExchangeCost = leftExchangeCost;
-                pn.rightExchangeCost = rightExchangeCost;
+                pn = new PlanNode(allPlans.size(), joinEnum, this, leftPlan, rightPlan, outerJoin);
+                pn.setJoinAndHintInfo(PlanNode.JoinMethod.HYBRID_HASH_JOIN, hashJoinExpr,
+                        HashJoinExpressionAnnotation.BuildSide.RIGHT, hintHashJoin);
+                pn.setJoinCosts(hjCost, totalCost, leftExchangeCost, rightExchangeCost);
+                planIndexesArray.add(pn.allPlansIndex);
                 allPlans.add(pn);
-                this.planIndexesArray.add(pn.allPlansIndex);
-                if (!forceEnum) {
-                    cheapestPlan = pn;
-                } else {
-                    cheapestPlan = findCheapestPlan();
-                }
-                this.cheapestPlanCost = cheapestPlan.totalCost;
-                this.cheapestPlanIndex = cheapestPlan.allPlansIndex;
+                setCheapestPlan(pn, forceEnum);
                 return pn.allPlansIndex;
             }
         }
         return PlanNode.NO_PLAN;
     }
 
-    private int buildBroadcastHashJoinPlan(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression hashJoinExpr, BroadcastExpressionAnnotation hintBroadcastHashJoin, boolean outerJoin) {
+    private int buildBroadcastHashJoinPlan(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression hashJoinExpr,
+            BroadcastExpressionAnnotation hintBroadcastHashJoin, boolean outerJoin) {
         List<PlanNode> allPlans = joinEnum.allPlans;
-        PlanNode pn, cheapestPlan;
+        JoinNode leftJn = leftPlan.getJoinNode();
+        JoinNode rightJn = rightPlan.getJoinNode();
+        PlanNode pn;
         ICost bcastHjCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;
 
         this.leftJn = leftJn;
         this.rightJn = rightJn;
 
-        if (nullExtendingSide(leftJn.datasetBits, outerJoin)) {
-            return PlanNode.NO_PLAN;
-        }
-
-        if (hashJoinExpr == null || hashJoinExpr == ConstantExpression.TRUE) {
-            return PlanNode.NO_PLAN;
-        }
-
-        if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_LEFTDEEP) && !leftJn.IsBaseLevelJoinNode()
-                && level > joinEnum.cboFullEnumLevel) {
-            return PlanNode.NO_PLAN;
-        }
-
-        if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_RIGHTDEEP)
-                && !rightJn.IsBaseLevelJoinNode() && level > joinEnum.cboFullEnumLevel) {
+        if (!hashJoinApplicable(leftJn, outerJoin, hashJoinExpr)) {
             return PlanNode.NO_PLAN;
         }
 
@@ -1017,65 +986,34 @@
                     .costAdd(allPlans.get(rightPlan.allPlansIndex).totalCost);
             totalCost = bcastHjCost.costAdd(rightExchangeCost).costAdd(childCosts);
             if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost) || forceEnum) {
-                pn = new PlanNode(allPlans.size(), joinEnum);
-                pn.setJoinNode(this);
-                pn.outerJoin = outerJoin;
-                pn.setLeftJoinIndex(leftJn.jnArrayIndex);
-                pn.setRightJoinIndex(rightJn.jnArrayIndex);
-                pn.setLeftPlanIndex(leftPlan.allPlansIndex);
-                pn.setRightPlanIndex(rightPlan.allPlansIndex);
-                pn.joinOp = PlanNode.JoinMethod.BROADCAST_HASH_JOIN; // need to check that all the conditions have equality predicates ONLY.
-                pn.joinHint = hintBroadcastHashJoin;
-                pn.numHintsUsed = allPlans.get(pn.getLeftPlanIndex()).numHintsUsed
-                        + allPlans.get(pn.getRightPlanIndex()).numHintsUsed;
-                if (hintBroadcastHashJoin != null) {
-                    pn.numHintsUsed++;
-                }
-                pn.side = HashJoinExpressionAnnotation.BuildSide.RIGHT;
-                pn.joinExpr = hashJoinExpr;
-                pn.opCost = bcastHjCost;
-                pn.totalCost = totalCost;
-                pn.leftExchangeCost = leftExchangeCost;
-                pn.rightExchangeCost = rightExchangeCost;
+                pn = new PlanNode(allPlans.size(), joinEnum, this, leftPlan, rightPlan, outerJoin);
+                pn.setJoinAndHintInfo(PlanNode.JoinMethod.BROADCAST_HASH_JOIN, hashJoinExpr,
+                        HashJoinExpressionAnnotation.BuildSide.RIGHT, hintBroadcastHashJoin);
+                pn.setJoinCosts(bcastHjCost, totalCost, leftExchangeCost, rightExchangeCost);
+                planIndexesArray.add(pn.allPlansIndex);
                 allPlans.add(pn);
-                this.planIndexesArray.add(pn.allPlansIndex);
-                if (!forceEnum) {
-                    cheapestPlan = pn;
-                } else {
-                    cheapestPlan = findCheapestPlan();
-                }
-                this.cheapestPlanCost = cheapestPlan.totalCost;
-                this.cheapestPlanIndex = cheapestPlan.allPlansIndex;
+                setCheapestPlan(pn, forceEnum);
                 return pn.allPlansIndex;
             }
         }
         return PlanNode.NO_PLAN;
     }
 
-    private int buildNLJoinPlan(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression nestedLoopJoinExpr, IndexedNLJoinExpressionAnnotation hintNLJoin, boolean outerJoin)
-            throws AlgebricksException {
+    private int buildNLJoinPlan(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression nestedLoopJoinExpr,
+            IndexedNLJoinExpressionAnnotation hintNLJoin, boolean outerJoin) throws AlgebricksException {
+        JoinNode leftJn = leftPlan.getJoinNode();
+        JoinNode rightJn = rightPlan.getJoinNode();
 
         // Build a nested loops plan, first check if it is possible
         // left right order must be preserved and right side should be a single data set
         List<PlanNode> allPlans = joinEnum.allPlans;
-        int numberOfTerms = joinEnum.numberOfTerms;
-        PlanNode pn, cheapestPlan;
+        PlanNode pn;
         ICost nljCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;
 
         this.leftJn = leftJn;
         this.rightJn = rightJn;
 
-        if (nullExtendingSide(leftJn.datasetBits, outerJoin)) {
-            return PlanNode.NO_PLAN;
-        }
-
-        if (rightJn.jnArrayIndex > numberOfTerms) {
-            // right side consists of more than one table
-            return PlanNode.NO_PLAN; // nested loop plan not possible.
-        }
-
-        if (nestedLoopJoinExpr == null || !rightJn.nestedLoopsApplicable(nestedLoopJoinExpr)) {
+        if (!NLJoinApplicable(leftJn, rightJn, outerJoin, nestedLoopJoinExpr)) {
             return PlanNode.NO_PLAN;
         }
 
@@ -1087,58 +1025,36 @@
         boolean forceEnum =
                 hintNLJoin != null || joinEnum.forceJoinOrderMode || outerJoin || level <= joinEnum.cboFullEnumLevel;
         if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost) || forceEnum) {
-            pn = new PlanNode(allPlans.size(), joinEnum);
-            pn.setJoinNode(this);
-            pn.outerJoin = outerJoin;
-            pn.setLeftJoinIndex(leftJn.jnArrayIndex);
-            pn.setRightJoinIndex(rightJn.jnArrayIndex);
-            pn.setLeftPlanIndex(leftPlan.allPlansIndex);
-            pn.setRightPlanIndex(rightPlan.allPlansIndex);
-            pn.joinOp = PlanNode.JoinMethod.INDEX_NESTED_LOOP_JOIN;
-            pn.joinHint = hintNLJoin;
-            pn.numHintsUsed = allPlans.get(pn.getLeftPlanIndex()).numHintsUsed
-                    + allPlans.get(pn.getRightPlanIndex()).numHintsUsed;
-            if (hintNLJoin != null) {
-                pn.numHintsUsed++;
-            }
-            pn.joinExpr = nestedLoopJoinExpr; // save it so can be used to add the NESTED annotation in getNewTree.
-            pn.opCost = nljCost;
-            pn.totalCost = totalCost;
-            pn.leftExchangeCost = leftExchangeCost;
-            pn.rightExchangeCost = rightExchangeCost;
+            pn = new PlanNode(allPlans.size(), joinEnum, this, leftPlan, rightPlan, outerJoin);
+
+            pn.setJoinAndHintInfo(PlanNode.JoinMethod.INDEX_NESTED_LOOP_JOIN, nestedLoopJoinExpr, null, hintNLJoin);
+            pn.setJoinCosts(nljCost, totalCost, leftExchangeCost, rightExchangeCost);
+            planIndexesArray.add(pn.allPlansIndex);
             allPlans.add(pn);
-            this.planIndexesArray.add(pn.allPlansIndex);
-            if (!forceEnum) {
-                cheapestPlan = pn;
-            } else {
-                cheapestPlan = findCheapestPlan();
-            }
-            this.cheapestPlanCost = cheapestPlan.totalCost;
-            this.cheapestPlanIndex = cheapestPlan.allPlansIndex;
+            setCheapestPlan(pn, forceEnum);
             return pn.allPlansIndex;
         }
         return PlanNode.NO_PLAN;
     }
 
-    private int buildCPJoinPlan(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression hashJoinExpr, ILogicalExpression nestedLoopJoinExpr, boolean outerJoin) {
+    private int buildCPJoinPlan(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression hashJoinExpr,
+            ILogicalExpression nestedLoopJoinExpr, boolean outerJoin) {
+        JoinNode leftJn = leftPlan.getJoinNode();
+        JoinNode rightJn = rightPlan.getJoinNode();
+
         // Now build a cartesian product nested loops plan
         List<PlanNode> allPlans = joinEnum.allPlans;
-        PlanNode pn, cheapestPlan;
+        PlanNode pn;
         ICost cpCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;
 
-        if (!joinEnum.cboCPEnumMode) {
-            return PlanNode.NO_PLAN;
-        }
-
-        if (nullExtendingSide(leftJn.datasetBits, outerJoin)) {
+        if (!CPJoinApplicable(leftJn, outerJoin)) {
             return PlanNode.NO_PLAN;
         }
 
         this.leftJn = leftJn;
         this.rightJn = rightJn;
 
-        ILogicalExpression cpJoinExpr = null;
+        ILogicalExpression cpJoinExpr;
         List<Integer> newJoinConditions = this.getNewJoinConditionsOnly();
         if (hashJoinExpr == null && nestedLoopJoinExpr == null) {
             cpJoinExpr = joinEnum.combineAllConditions(newJoinConditions);
@@ -1164,28 +1080,13 @@
         totalCost = cpCost.costAdd(rightExchangeCost).costAdd(childCosts);
         boolean forceEnum = joinEnum.forceJoinOrderMode || outerJoin || level <= joinEnum.cboFullEnumLevel;
         if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost) || forceEnum) {
-            pn = new PlanNode(allPlans.size(), joinEnum);
-            pn.setJoinNode(this);
-            pn.outerJoin = outerJoin;
-            pn.setLeftJoinIndex(leftJn.jnArrayIndex);
-            pn.setRightJoinIndex(rightJn.jnArrayIndex);
-            pn.setLeftPlanIndex(leftPlan.allPlansIndex);
-            pn.setRightPlanIndex(rightPlan.allPlansIndex);
-            pn.joinOp = PlanNode.JoinMethod.CARTESIAN_PRODUCT_JOIN;
-            pn.joinExpr = Objects.requireNonNullElse(cpJoinExpr, ConstantExpression.TRUE);
-            pn.opCost = cpCost;
-            pn.totalCost = totalCost;
-            pn.leftExchangeCost = leftExchangeCost;
-            pn.rightExchangeCost = rightExchangeCost;
+            pn = new PlanNode(allPlans.size(), joinEnum, this, leftPlan, rightPlan, outerJoin);
+            pn.setJoinAndHintInfo(PlanNode.JoinMethod.CARTESIAN_PRODUCT_JOIN,
+                    Objects.requireNonNullElse(cpJoinExpr, ConstantExpression.TRUE), null, null);
+            pn.setJoinCosts(cpCost, totalCost, leftExchangeCost, rightExchangeCost);
+            planIndexesArray.add(pn.allPlansIndex);
             allPlans.add(pn);
-            this.planIndexesArray.add(pn.allPlansIndex);
-            if (!forceEnum) {
-                cheapestPlan = pn;
-            } else {
-                cheapestPlan = findCheapestPlan();
-            }
-            this.cheapestPlanCost = cheapestPlan.totalCost;
-            this.cheapestPlanIndex = cheapestPlan.allPlansIndex;
+            setCheapestPlan(pn, forceEnum);
             return pn.allPlansIndex;
         }
         return PlanNode.NO_PLAN;
@@ -1202,7 +1103,7 @@
             }
             leftPlan = joinEnum.allPlans.get(leftJn.cheapestPlanIndex);
             rightPlan = joinEnum.allPlans.get(rightJn.cheapestPlanIndex);
-            addMultiDatasetPlans(leftJn, rightJn, leftPlan, rightPlan);
+            addMultiDatasetPlans(leftPlan, rightPlan);
         } else {
             // FOR JOIN NODE LEVELS LESS THAN OR EQUAL TO THE LEVEL SPECIFIED FOR FULL ENUMERATION,
             // DO FULL ENUMERATION => DO NOT PRUNE
@@ -1210,14 +1111,16 @@
                 leftPlan = joinEnum.allPlans.get(leftPlanIndex);
                 for (int rightPlanIndex : rightJn.planIndexesArray) {
                     rightPlan = joinEnum.allPlans.get(rightPlanIndex);
-                    addMultiDatasetPlans(leftJn, rightJn, leftPlan, rightPlan);
+                    addMultiDatasetPlans(leftPlan, rightPlan);
                 }
             }
         }
     }
 
-    protected void addMultiDatasetPlans(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan)
-            throws AlgebricksException {
+    protected void addMultiDatasetPlans(PlanNode leftPlan, PlanNode rightPlan) throws AlgebricksException {
+        JoinNode leftJn = leftPlan.getJoinNode();
+        JoinNode rightJn = rightPlan.getJoinNode();
+
         this.leftJn = leftJn;
         this.rightJn = rightJn;
 
@@ -1273,19 +1176,19 @@
         IndexedNLJoinExpressionAnnotation hintNLJoin = joinEnum.findNLJoinHint(newJoinConditions);
 
         if (hintHashJoin != null) {
-            validPlan = buildHintedHJPlans(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr, hintHashJoin, outerJoin,
-                    newJoinConditions);
+            validPlan =
+                    buildHintedHJPlans(leftPlan, rightPlan, hashJoinExpr, hintHashJoin, outerJoin, newJoinConditions);
         } else if (hintBroadcastHashJoin != null) {
-            validPlan = buildHintedBcastHJPlans(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr,
-                    hintBroadcastHashJoin, outerJoin, newJoinConditions);
+            validPlan = buildHintedBcastHJPlans(leftPlan, rightPlan, hashJoinExpr, hintBroadcastHashJoin, outerJoin,
+                    newJoinConditions);
         } else if (hintNLJoin != null) {
-            validPlan = buildHintedNLJPlans(leftJn, rightJn, leftPlan, rightPlan, nestedLoopJoinExpr, hintNLJoin,
-                    outerJoin, newJoinConditions);
+            validPlan = buildHintedNLJPlans(leftPlan, rightPlan, nestedLoopJoinExpr, hintNLJoin, outerJoin,
+                    newJoinConditions);
         }
 
         if (!validPlan) {
             // No join hints or inapplicable hinted plans, try all non hinted plans.
-            buildAllNonHintedPlans(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr, nestedLoopJoinExpr, outerJoin);
+            buildAllNonHintedPlans(leftPlan, rightPlan, hashJoinExpr, nestedLoopJoinExpr, outerJoin);
         }
 
         //Reset as these might have changed when we tried the commutative joins.
@@ -1293,9 +1196,11 @@
         this.rightJn = rightJn;
     }
 
-    private boolean buildHintedHJPlans(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression hashJoinExpr, HashJoinExpressionAnnotation hintHashJoin, boolean outerJoin,
-            List<Integer> newJoinConditions) {
+    private boolean buildHintedHJPlans(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression hashJoinExpr,
+            HashJoinExpressionAnnotation hintHashJoin, boolean outerJoin, List<Integer> newJoinConditions) {
+        JoinNode leftJn = leftPlan.getJoinNode();
+        JoinNode rightJn = rightPlan.getJoinNode();
+
         int hjPlan, commutativeHjPlan;
         hjPlan = commutativeHjPlan = PlanNode.NO_PLAN;
         boolean build = (hintHashJoin.getBuildOrProbe() == HashJoinExpressionAnnotation.BuildOrProbe.BUILD);
@@ -1312,22 +1217,22 @@
                     || rightJn.aliases.contains(buildOrProbeObject)))
                     || (probe && (leftJn.datasetNames.contains(buildOrProbeObject)
                             || leftJn.aliases.contains(buildOrProbeObject)))) {
-                hjPlan = buildHashJoinPlan(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr, hintHashJoin, outerJoin);
+                hjPlan = buildHashJoinPlan(leftPlan, rightPlan, hashJoinExpr, hintHashJoin, outerJoin);
             } else if ((build && (leftJn.datasetNames.contains(buildOrProbeObject)
                     || leftJn.aliases.contains(buildOrProbeObject)))
                     || (probe && (rightJn.datasetNames.contains(buildOrProbeObject)
                             || rightJn.aliases.contains(buildOrProbeObject)))) {
-                commutativeHjPlan =
-                        buildHashJoinPlan(rightJn, leftJn, rightPlan, leftPlan, hashJoinExpr, hintHashJoin, outerJoin);
+                commutativeHjPlan = buildHashJoinPlan(rightPlan, leftPlan, hashJoinExpr, hintHashJoin, outerJoin);
             }
         }
 
         return handleHints(hjPlan, commutativeHjPlan, hintHashJoin, newJoinConditions);
     }
 
-    private boolean buildHintedBcastHJPlans(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression hashJoinExpr, BroadcastExpressionAnnotation hintBroadcastHashJoin, boolean outerJoin,
-            List<Integer> newJoinConditions) {
+    private boolean buildHintedBcastHJPlans(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression hashJoinExpr,
+            BroadcastExpressionAnnotation hintBroadcastHashJoin, boolean outerJoin, List<Integer> newJoinConditions) {
+        JoinNode leftJn = leftPlan.getJoinNode();
+        JoinNode rightJn = rightPlan.getJoinNode();
         int bcastHjPlan, commutativeBcastHjPlan;
         bcastHjPlan = commutativeBcastHjPlan = PlanNode.NO_PLAN;
         boolean validBroadcastObject = false;
@@ -1339,33 +1244,32 @@
         }
         if (validBroadcastObject) {
             if (rightJn.datasetNames.contains(broadcastObject) || rightJn.aliases.contains(broadcastObject)) {
-                bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr,
-                        hintBroadcastHashJoin, outerJoin);
+                bcastHjPlan =
+                        buildBroadcastHashJoinPlan(leftPlan, rightPlan, hashJoinExpr, hintBroadcastHashJoin, outerJoin);
             } else if (leftJn.datasetNames.contains(broadcastObject) || leftJn.aliases.contains(broadcastObject)) {
-                commutativeBcastHjPlan = buildBroadcastHashJoinPlan(rightJn, leftJn, rightPlan, leftPlan, hashJoinExpr,
-                        hintBroadcastHashJoin, outerJoin);
+                commutativeBcastHjPlan =
+                        buildBroadcastHashJoinPlan(rightPlan, leftPlan, hashJoinExpr, hintBroadcastHashJoin, outerJoin);
             }
         } else if (broadcastObject == null) {
-            bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr,
-                    hintBroadcastHashJoin, outerJoin);
+            bcastHjPlan =
+                    buildBroadcastHashJoinPlan(leftPlan, rightPlan, hashJoinExpr, hintBroadcastHashJoin, outerJoin);
             if (!joinEnum.forceJoinOrderMode || level <= joinEnum.cboFullEnumLevel) {
-                commutativeBcastHjPlan = buildBroadcastHashJoinPlan(rightJn, leftJn, rightPlan, leftPlan, hashJoinExpr,
-                        hintBroadcastHashJoin, outerJoin);
+                commutativeBcastHjPlan =
+                        buildBroadcastHashJoinPlan(rightPlan, leftPlan, hashJoinExpr, hintBroadcastHashJoin, outerJoin);
             }
         }
 
         return handleHints(bcastHjPlan, commutativeBcastHjPlan, hintBroadcastHashJoin, newJoinConditions);
     }
 
-    private boolean buildHintedNLJPlans(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression nestedLoopJoinExpr, IndexedNLJoinExpressionAnnotation hintNLJoin, boolean outerJoin,
-            List<Integer> newJoinConditions) throws AlgebricksException {
+    private boolean buildHintedNLJPlans(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression nestedLoopJoinExpr,
+            IndexedNLJoinExpressionAnnotation hintNLJoin, boolean outerJoin, List<Integer> newJoinConditions)
+            throws AlgebricksException {
         int nljPlan, commutativeNljPlan;
         nljPlan = commutativeNljPlan = PlanNode.NO_PLAN;
-        nljPlan = buildNLJoinPlan(leftJn, rightJn, leftPlan, rightPlan, nestedLoopJoinExpr, hintNLJoin, outerJoin);
+        nljPlan = buildNLJoinPlan(leftPlan, rightPlan, nestedLoopJoinExpr, hintNLJoin, outerJoin);
         if (!joinEnum.forceJoinOrderMode || level <= joinEnum.cboFullEnumLevel) {
-            commutativeNljPlan =
-                    buildNLJoinPlan(rightJn, leftJn, rightPlan, leftPlan, nestedLoopJoinExpr, hintNLJoin, outerJoin);
+            commutativeNljPlan = buildNLJoinPlan(rightPlan, leftPlan, nestedLoopJoinExpr, hintNLJoin, outerJoin);
         }
 
         return handleHints(nljPlan, commutativeNljPlan, hintNLJoin, newJoinConditions);
@@ -1390,24 +1294,24 @@
         return true;
     }
 
-    private void buildAllNonHintedPlans(JoinNode leftJn, JoinNode rightJn, PlanNode leftPlan, PlanNode rightPlan,
-            ILogicalExpression hashJoinExpr, ILogicalExpression nestedLoopJoinExpr, boolean outerJoin)
-            throws AlgebricksException {
-        buildHashJoinPlan(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr, null, outerJoin);
+    private void buildAllNonHintedPlans(PlanNode leftPlan, PlanNode rightPlan, ILogicalExpression hashJoinExpr,
+            ILogicalExpression nestedLoopJoinExpr, boolean outerJoin) throws AlgebricksException {
+
+        buildHashJoinPlan(leftPlan, rightPlan, hashJoinExpr, null, outerJoin);
         if (!joinEnum.forceJoinOrderMode || level <= joinEnum.cboFullEnumLevel) {
-            buildHashJoinPlan(rightJn, leftJn, rightPlan, leftPlan, hashJoinExpr, null, outerJoin);
+            buildHashJoinPlan(rightPlan, leftPlan, hashJoinExpr, null, outerJoin);
         }
-        buildBroadcastHashJoinPlan(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr, null, outerJoin);
+        buildBroadcastHashJoinPlan(leftPlan, rightPlan, hashJoinExpr, null, outerJoin);
         if (!joinEnum.forceJoinOrderMode || level <= joinEnum.cboFullEnumLevel) {
-            buildBroadcastHashJoinPlan(rightJn, leftJn, rightPlan, leftPlan, hashJoinExpr, null, outerJoin);
+            buildBroadcastHashJoinPlan(rightPlan, leftPlan, hashJoinExpr, null, outerJoin);
         }
-        buildNLJoinPlan(leftJn, rightJn, leftPlan, rightPlan, nestedLoopJoinExpr, null, outerJoin);
+        buildNLJoinPlan(leftPlan, rightPlan, nestedLoopJoinExpr, null, outerJoin);
         if (!joinEnum.forceJoinOrderMode || level <= joinEnum.cboFullEnumLevel) {
-            buildNLJoinPlan(rightJn, leftJn, rightPlan, leftPlan, nestedLoopJoinExpr, null, outerJoin);
+            buildNLJoinPlan(rightPlan, leftPlan, nestedLoopJoinExpr, null, outerJoin);
         }
-        buildCPJoinPlan(leftJn, rightJn, leftPlan, rightPlan, hashJoinExpr, nestedLoopJoinExpr, outerJoin);
+        buildCPJoinPlan(leftPlan, rightPlan, hashJoinExpr, nestedLoopJoinExpr, outerJoin);
         if (!joinEnum.forceJoinOrderMode || level <= joinEnum.cboFullEnumLevel) {
-            buildCPJoinPlan(rightJn, leftJn, rightPlan, leftPlan, hashJoinExpr, nestedLoopJoinExpr, outerJoin);
+            buildCPJoinPlan(rightPlan, leftPlan, hashJoinExpr, nestedLoopJoinExpr, outerJoin);
         }
     }
 
@@ -1473,6 +1377,12 @@
         return cheapestPlanNode;
     }
 
+    private void setCheapestPlan(PlanNode pn, boolean forceEnum) {
+        PlanNode cheapestPlan = forceEnum ? findCheapestPlan() : pn;
+        cheapestPlanCost = cheapestPlan.totalCost;
+        cheapestPlanIndex = cheapestPlan.allPlansIndex;
+    }
+
     @Override
     public String toString() {
         List<PlanNode> allPlans = joinEnum.getAllPlans();
@@ -1498,12 +1408,11 @@
         sb.append("level ").append(level).append('\n');
         sb.append("highestDatasetId ").append(highestDatasetId).append('\n');
         if (IsBaseLevelJoinNode()) {
-            sb.append("orig cardinality ").append((double) Math.round(origCardinality * 100) / 100).append('\n');
+            sb.append("orig cardinality ").append(dumpDouble(origCardinality));
         }
-        sb.append("cardinality ").append((double) Math.round(cardinality * 100) / 100).append('\n');
-        sb.append("size ").append((double) Math.round(size * 100) / 100).append('\n');
-        sb.append("outputSize(sizeVarsAfterScan) ").append((double) Math.round(sizeVarsAfterScan * 100) / 100)
-                .append('\n');
+        sb.append("cardinality ").append(dumpDouble(cardinality));
+        sb.append("size ").append(dumpDouble(size));
+        sb.append("outputSize(sizeVarsAfterScan) ").append(dumpDouble(sizeVarsAfterScan));
         if (planIndexesArray.size() == 0) {
             sb.append("No plans considered for this join node").append('\n');
         } else {
@@ -1541,9 +1450,11 @@
                     sb.append(dumpLeftRightPlanCosts(pn));
                 }
             }
-            sb.append("\nCheapest plan for JoinNode ").append(jnArrayIndex).append(" is ").append(cheapestPlanIndex)
-                    .append(", cost is ").append(allPlans.get(cheapestPlanIndex).getTotalCost().computeTotalCost())
-                    .append('\n');
+            if (cheapestPlanIndex != PlanNode.NO_PLAN) {
+                sb.append("\nCheapest plan for JoinNode ").append(jnArrayIndex).append(" is ").append(cheapestPlanIndex)
+                        .append(", cost is ")
+                        .append(dumpDouble(allPlans.get(cheapestPlanIndex).getTotalCost().computeTotalCost()));
+            }
         }
         sb.append("-----------------------------------------------------------------").append('\n');
         return sb.toString();
@@ -1551,7 +1462,19 @@
 
     protected String dumpCost(ICost cost) {
         StringBuilder sb = new StringBuilder(128);
-        sb.append(cost.computeTotalCost()).append('\n');
+        sb.append((double) Math.round(cost.computeTotalCost() * 100) / 100).append('\n');
+        return sb.toString();
+    }
+
+    protected String dumpDouble(double val) {
+        StringBuilder sb = new StringBuilder(128);
+        sb.append((double) Math.round(val * 100) / 100).append('\n');
+        return sb.toString();
+    }
+
+    protected String dumpDoubleNoNewline(double val) {
+        StringBuilder sb = new StringBuilder(128);
+        sb.append((double) Math.round(val * 100) / 100);
         return sb.toString();
     }
 
@@ -1562,8 +1485,8 @@
         ICost leftPlanTotalCost = leftPlan.getTotalCost();
         ICost rightPlanTotalCost = rightPlan.getTotalCost();
 
-        sb.append("  left plan cost ").append(leftPlanTotalCost.computeTotalCost()).append(", right plan cost ")
-                .append(rightPlanTotalCost.computeTotalCost()).append('\n');
+        sb.append("  left plan cost ").append(dumpDoubleNoNewline(leftPlanTotalCost.computeTotalCost()))
+                .append(", right plan cost ").append(dumpDouble(rightPlanTotalCost.computeTotalCost()));
         return sb.toString();
     }
 
@@ -1573,13 +1496,13 @@
         int lowestCostPlanIndex = 0;
         for (int planIndex : planIndexesArray) {
             ICost planCost = allPlans.get(planIndex).totalCost;
-            sb.append("plan ").append(planIndex).append(" cost is ").append(planCost.computeTotalCost()).append('\n');
+            sb.append("plan ").append(planIndex).append(" cost is ").append(dumpDouble(planCost.computeTotalCost()));
             if (planCost.costLT(minCost)) {
                 minCost = planCost;
                 lowestCostPlanIndex = planIndex;
             }
         }
         sb.append("Cheapest Plan is ").append(lowestCostPlanIndex).append(", Cost is ")
-                .append(minCost.computeTotalCost()).append('\n');
+                .append(dumpDouble(minCost.computeTotalCost()));
     }
 }
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/PlanNode.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/PlanNode.java
index ba02c1a..e514f5e 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/PlanNode.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/PlanNode.java
@@ -19,41 +19,49 @@
 
 package org.apache.asterix.optimizer.rules.cbo;
 
+import java.util.List;
+
+import org.apache.asterix.metadata.entities.Index;
 import org.apache.asterix.optimizer.cost.ICost;
 import org.apache.hyracks.algebricks.common.utils.Pair;
+import org.apache.hyracks.algebricks.common.utils.Triple;
 import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
 import org.apache.hyracks.algebricks.core.algebra.base.ILogicalOperator;
+import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
 import org.apache.hyracks.algebricks.core.algebra.expressions.HashJoinExpressionAnnotation;
 import org.apache.hyracks.algebricks.core.algebra.expressions.IExpressionAnnotation;
-import org.apache.hyracks.algebricks.core.algebra.operators.logical.DataSourceScanOperator;
 
 public class PlanNode {
 
     protected static int NO_PLAN = -1;
 
     private final JoinEnum joinEnum;
+
+    protected String datasetName;
+
+    protected ILogicalOperator leafInput;
+
+    protected JoinNode jn;
     protected boolean outerJoin;
     protected int allPlansIndex;
-    protected int[] planIndexes;
     protected int[] jnIndexes;
-    protected JoinNode jn;
-    protected String datasetName;
+    protected int[] planIndexes;
+
+    protected ScanMethod scanOp;
+    protected boolean indexHint;
+
+    protected JoinMethod joinOp;
+
+    protected ILogicalExpression joinExpr;
+
+    // Used to indicate which side to build for HJ and which side to broadcast for BHJ.
+    protected HashJoinExpressionAnnotation.BuildSide side;
+    protected IExpressionAnnotation joinHint;
+    protected int numHintsUsed;
     protected ICost opCost;
     protected ICost totalCost;
     protected ICost leftExchangeCost;
     protected ICost rightExchangeCost;
-    protected JoinMethod joinOp;
-    protected boolean indexHint;
-    protected IExpressionAnnotation joinHint;
-
-    protected int numHintsUsed;
-
-    // Used to indicate which side to build for HJ and which side to broadcast for BHJ.
-    protected HashJoinExpressionAnnotation.BuildSide side;
-    protected ScanMethod scanOp;
-    protected ILogicalExpression joinExpr;
-    private DataSourceScanOperator correspondingDataSourceScanOp;
-    protected ILogicalOperator leafInput;
 
     public enum ScanMethod {
         INDEX_SCAN,
@@ -67,17 +75,6 @@
         CARTESIAN_PRODUCT_JOIN
     }
 
-    public PlanNode(int planIndex, JoinEnum joinE) {
-        this.allPlansIndex = planIndex;
-        joinEnum = joinE;
-        planIndexes = new int[2]; // 0 is for left, 1 is for right
-        jnIndexes = new int[2]; // join node index(es)
-        outerJoin = false;
-        indexHint = false;
-        joinHint = null;
-        numHintsUsed = 0;
-    }
-
     public int getIndex() {
         return allPlansIndex;
     }
@@ -169,10 +166,6 @@
         this.datasetName = dsName;
     }
 
-    private DataSourceScanOperator getDataSourceScanOp() {
-        return correspondingDataSourceScanOp; // This applies only to singleDataSetPlans
-    }
-
     protected ILogicalOperator getLeafInput() {
         return leafInput; // This applies only to singleDataSetPlans
     }
@@ -228,4 +221,92 @@
     public ILogicalExpression getJoinExpr() {
         return joinExpr;
     }
+
+    // Constructor for DUMMY plan nodes.
+    public PlanNode(int planIndex, JoinEnum joinE) {
+        this.allPlansIndex = planIndex;
+        joinEnum = joinE;
+        jn = null;
+        planIndexes = new int[2]; // 0 is for left, 1 is for right
+        jnIndexes = new int[2]; // join node index(es)
+        setLeftJoinIndex(JoinNode.NO_JN);
+        setRightJoinIndex(JoinNode.NO_JN);
+        setLeftPlanIndex(PlanNode.NO_PLAN);
+        setRightPlanIndex(PlanNode.NO_PLAN);
+        opCost = totalCost = joinEnum.getCostHandle().zeroCost();
+        outerJoin = false;
+        indexHint = false;
+        joinHint = null;
+        numHintsUsed = 0;
+    }
+
+    // Constructor for SCAN plan nodes.
+    public PlanNode(int planIndex, JoinEnum joinE, JoinNode joinNode, String datasetName, ILogicalOperator leafInput) {
+        this.allPlansIndex = planIndex;
+        joinEnum = joinE;
+        setJoinNode(joinNode);
+        this.datasetName = datasetName;
+        this.leafInput = leafInput;
+        planIndexes = new int[2]; // 0 is for left, 1 is for right
+        jnIndexes = new int[2]; // join node index(es)
+        setLeftJoinIndex(jn.jnArrayIndex);
+        setRightJoinIndex(JoinNode.NO_JN);
+        setLeftPlanIndex(PlanNode.NO_PLAN); // There ane no plans below this plan.
+        setRightPlanIndex(PlanNode.NO_PLAN); // There ane no plans below this plan.
+        indexHint = false;
+        joinHint = null;
+        numHintsUsed = 0;
+    }
+
+    // Constructor for JOIN plan nodes.
+    public PlanNode(int planIndex, JoinEnum joinE, JoinNode joinNode, PlanNode leftPlan, PlanNode rightPlan,
+            boolean outerJoin) {
+        this.allPlansIndex = planIndex;
+        joinEnum = joinE;
+        setJoinNode(joinNode);
+        this.outerJoin = outerJoin;
+        planIndexes = new int[2]; // 0 is for left, 1 is for right
+        jnIndexes = new int[2]; // join node index(es)
+        setLeftJoinIndex(leftPlan.jn.jnArrayIndex);
+        setRightJoinIndex(rightPlan.jn.jnArrayIndex);
+        setLeftPlanIndex(leftPlan.allPlansIndex);
+        setRightPlanIndex(rightPlan.allPlansIndex);
+        indexHint = false;
+        joinHint = null;
+        numHintsUsed = 0;
+    }
+
+    protected void setScanAndHintInfo(ScanMethod scanMethod,
+            List<Triple<Index, Double, AbstractFunctionCallExpression>> mandatoryIndexesInfo) {
+        setScanMethod(scanMethod);
+        if (mandatoryIndexesInfo.size() > 0) {
+            indexHint = true;
+            numHintsUsed = 1;
+        }
+    }
+
+    protected void setScanCosts(ICost opCost) {
+        this.opCost = opCost;
+        this.totalCost = opCost;
+    }
+
+    protected void setJoinAndHintInfo(JoinMethod joinMethod, ILogicalExpression joinExpr,
+            HashJoinExpressionAnnotation.BuildSide side, IExpressionAnnotation hint) {
+        joinOp = joinMethod;
+        this.joinExpr = joinExpr;
+        this.side = side;
+        joinHint = hint;
+        numHintsUsed = joinEnum.allPlans.get(getLeftPlanIndex()).numHintsUsed
+                + joinEnum.allPlans.get(getRightPlanIndex()).numHintsUsed;
+        if (hint != null) {
+            numHintsUsed++;
+        }
+    }
+
+    protected void setJoinCosts(ICost opCost, ICost totalCost, ICost leftExchangeCost, ICost rightExchangeCost) {
+        this.opCost = opCost;
+        this.totalCost = totalCost;
+        this.leftExchangeCost = leftExchangeCost;
+        this.rightExchangeCost = rightExchangeCost;
+    }
 }
diff --git a/asterixdb/asterix-app/src/test/resources/optimizerts/results_cbo/joins/nlj_partitioning_property_1.plan b/asterixdb/asterix-app/src/test/resources/optimizerts/results_cbo/joins/nlj_partitioning_property_1.plan
index 5f3c681..91a6aca 100644
--- a/asterixdb/asterix-app/src/test/resources/optimizerts/results_cbo/joins/nlj_partitioning_property_1.plan
+++ b/asterixdb/asterix-app/src/test/resources/optimizerts/results_cbo/joins/nlj_partitioning_property_1.plan
@@ -6,24 +6,26 @@
           -- RANDOM_MERGE_EXCHANGE  |PARTITIONED|
             -- AGGREGATE  |PARTITIONED|
               -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                -- HYBRID_HASH_JOIN [$$76][$$78]  |PARTITIONED|
+                -- NESTED_LOOP  |PARTITIONED|
                   -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                    -- NESTED_LOOP  |PARTITIONED|
+                    -- STREAM_PROJECT  |PARTITIONED|
                       -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                        -- STREAM_PROJECT  |PARTITIONED|
+                        -- HYBRID_HASH_JOIN [$$76][$$78]  |PARTITIONED|
                           -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                            -- DATASOURCE_SCAN (tpch.Supplier)  |PARTITIONED|
+                            -- STREAM_PROJECT  |PARTITIONED|
                               -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                                -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
-                      -- BROADCAST_EXCHANGE  |PARTITIONED|
-                        -- STREAM_PROJECT  |PARTITIONED|
-                          -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                            -- DATASOURCE_SCAN (tpch.Part)  |PARTITIONED|
+                                -- DATASOURCE_SCAN (tpch.Supplier)  |PARTITIONED|
+                                  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                    -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
+                          -- BROADCAST_EXCHANGE  |PARTITIONED|
+                            -- STREAM_PROJECT  |PARTITIONED|
                               -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                                -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
+                                -- DATASOURCE_SCAN (tpch.Partsupp)  |PARTITIONED|
+                                  -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
+                                    -- EMPTY_TUPLE_SOURCE  |PARTITIONED|
                   -- BROADCAST_EXCHANGE  |PARTITIONED|
                     -- STREAM_PROJECT  |PARTITIONED|
                       -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
-                        -- DATASOURCE_SCAN (tpch.Partsupp)  |PARTITIONED|
+                        -- DATASOURCE_SCAN (tpch.Part)  |PARTITIONED|
                           -- ONE_TO_ONE_EXCHANGE  |PARTITIONED|
                             -- EMPTY_TUPLE_SOURCE  |PARTITIONED|