/***************************************************************************** * Copyright (C) 2008 EnterpriseDB Corporation. * Copyright (C) 2011 Stado Global Development Group. * * This file is part of Stado. * * Stado is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Stado 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Stado. If not, see <http://www.gnu.org/licenses/>. * * You can find Stado at http://www.stado.us * ****************************************************************************/ /* * ExecutionPlan.java * * */ package org.postgresql.stado.planner; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.postgresql.stado.common.util.Property; import org.postgresql.stado.engine.XDBSessionContext; import org.postgresql.stado.exception.XDBServerException; import org.postgresql.stado.metadata.DBNode; import org.postgresql.stado.metadata.SysColumn; import org.postgresql.stado.metadata.SysTable; import org.postgresql.stado.parser.ExpressionType; /** * Converts a QueryPlan into an ExecutionPlan, one that prepares the nodes with * Step information * */ public class ExecutionPlan { // whether or not to do select projections from // (select endstuff from lasttable) at the end. // Some databases do not like it. public static final boolean TRANSFORM_PROJECTIONS = Property.getBoolean( "xdb.transformProjections", false); /** This subplans parent plan */ protected ExecutionPlan parentExecutionPlan; /** The individual steps in this plan */ public List<ExecutionStep> stepList; /** The final projections on the last step */ public String finalProjString = ""; /** the final temp table name on the last step */ public String finalTempTableName = ""; /** if this is a union, the individual plans for statements unioned */ public List<ExecutionPlan> unionPlanList; /** Subqueries that are scalar (return one value) */ public List<ExecutionPlan> scalarPlanList; /** Relational subqueries (FROM clause) */ public List<ExecutionPlan> relationPlanList; /** Used for saving the results of a scalar query */ public String scalarResult = "null"; /** scalar index */ public int scalarPlaceholderNo; /** whether or not we combine the results at the coordinator */ public boolean combineResults = false; /** whether or not it is a top level union */ public boolean isTopLevelUnion = false; /** whether or not this is the final unioned query */ public boolean isFinalUnionPart = false; /** union type */ public int unionType = QueryPlan.UNIONTYPE_NONE; /** list of temp tables to drop at the nodes after this plan */ public List<String> nodeTempTableDropList; /** list of "temp" tables on the coordinator to drop after this plan */ public List<String> coordTempTableDropList; /** The correlated depth of this plan */ public int correlatedDepth = 0; /** whether or not we can hash by the correlated column to reduce * sending rows. */ protected boolean isCorrelatedHashable = false; /** session info */ private XDBSessionContext client; /** helper for PreparedStatements */ protected ExecutionPlanPreparedHandler anEPParameterHelper; private long finalLimit = -1; private long finalOffset = -1; /** * Creates a new instance of ExecutionPlan * * @param parentExecPlan * @param aQueryPlan * @param parentNodeUsageTable * @param parentTargetNodeList * @param isUnionSubquery * @param client */ public ExecutionPlan(ExecutionPlan parentExecPlan, QueryPlan aQueryPlan, HashMap<Integer, NodeUsage> parentNodeUsageTable, List<DBNode> parentTargetNodeList, boolean isUnionSubquery, XDBSessionContext client) { this(parentExecPlan, aQueryPlan, parentNodeUsageTable, parentTargetNodeList, isUnionSubquery, client, 0, false); } /** * Constructor * * @param parentExecPlan * @param aQueryPlan * @param parentNodeUsageTable * @param parentTargetNodeList * @param isUnionSubquery * @param client * @param correlatedDepth * @param isCorrelatedHashable */ public ExecutionPlan(ExecutionPlan parentExecPlan, QueryPlan aQueryPlan, HashMap<Integer, NodeUsage> parentNodeUsageTable, List<DBNode> parentTargetNodeList, boolean isUnionSubquery, XDBSessionContext client, int correlatedDepth, boolean isCorrelatedHashable) { Leaf currentLeaf; Leaf nextLeaf; Leaf nextNextLeaf; Leaf previousLeaf = null; ExecutionStep anExecutionStep; ExecutionPlan subExecPlan; this.parentExecutionPlan = parentExecPlan; this.client = client; this.correlatedDepth = correlatedDepth; this.isCorrelatedHashable = isCorrelatedHashable; stepList = new ArrayList<ExecutionStep>(); unionPlanList = new ArrayList<ExecutionPlan>(); scalarPlanList = new ArrayList<ExecutionPlan>(); relationPlanList = new ArrayList<ExecutionPlan>(); this.unionType = aQueryPlan.unionType; // See if we are dealing with UNIONs if (aQueryPlan.isUnion) { buildUnionPlan(aQueryPlan, parentNodeUsageTable, parentTargetNodeList); return; } // Now check for subqueries that are relation or scalar subqueries for (QueryPlan subQueryPlan : aQueryPlan.subplanList) { subExecPlan = new ExecutionPlan(this, subQueryPlan, parentNodeUsageTable, parentTargetNodeList, false, client); if (subQueryPlan.scalarLeaf != null) { // save scalar-related info we will need for execution. subExecPlan.scalarPlaceholderNo = subQueryPlan.placeHolderNo; scalarPlanList.add(subExecPlan); } else { // must be relation relationPlanList.add(subExecPlan); } } finalProjString = aQueryPlan.finalProjString; // loop through all the steps // get the select statement, and execute. aQueryPlan.initLeafIteration(); // We need to peak ahead 2 leaves to correctly build up the steps. for (currentLeaf = aQueryPlan.nextLeaf(), nextLeaf = aQueryPlan .nextLeaf(); currentLeaf != null; previousLeaf = currentLeaf, currentLeaf = nextLeaf, nextLeaf = nextNextLeaf) { // peek ahead nextNextLeaf = aQueryPlan.nextLeaf(); anExecutionStep = createNewStep(); anExecutionStep.convertFromLeaf(currentLeaf, nextLeaf, nextNextLeaf, previousLeaf, parentNodeUsageTable, parentTargetNodeList, isUnionSubquery, aQueryPlan.planType); finalTempTableName = currentLeaf.getTargetTableName(); /* * Handle the case where we are on the very last step (top level of * query, not subquery). We don't want to create a final temp table * and instead combine ResultSets. * * We want to do that for non-aggregate queries, and for aggregate * queries that include a group by. For aggregate queries without a * group by, we still want them to go to the coordinator * (SELECT COUNT(*) FROM t). * * If the final step is being executed on the coordinator, we don't * want to bother creating a "final" table, we just set up the final * ResultSet, which is done later in QueryProcessor. */ if (nextLeaf == null && parentTargetNodeList == null && (aQueryPlan.isTopLevelPlan || aQueryPlan.isFinalUnionPart)) { isFinalUnionPart = aQueryPlan.isFinalUnionPart; // see if combining from nodes if (anExecutionStep.aStepDetail.isProducer && !(currentLeaf.isCombinerStep() && currentLeaf.groupByColumns .size() == 0)) { // Flag for combining results if (isFinalUnionPart) { anExecutionStep.aStepDetail.isFinalUnionPart = true; anExecutionStep.aStepDetail.finalUnionPartSortInfo = aQueryPlan.sortInfo; anExecutionStep.aStepDetail.finalUnionPartIsDistinct = aQueryPlan.isDistinct; } anExecutionStep.coordStepDetail = null; anExecutionStep.isFinalStep = true; anExecutionStep.aStepDetail.setDestTypeCoordinatorFinal(); modifyFinalSelectOnNodes(anExecutionStep, aQueryPlan, TRANSFORM_PROJECTIONS); } else { // We just want the last step to execute at the combiner, // without putting it into a temp table. // Flag for combining results if (isFinalUnionPart) { anExecutionStep.coordStepDetail.isFinalUnionPart = true; anExecutionStep.coordStepDetail.finalUnionPartSortInfo = aQueryPlan.sortInfo; anExecutionStep.coordStepDetail.finalUnionPartIsDistinct = aQueryPlan.isDistinct; } anExecutionStep.coordStepDetail.isConsumer = false; anExecutionStep.isFinalStep = true; anExecutionStep.coordStepDetail.setDestTypeCoordinatorFinal(); anExecutionStep.coordStepDetail.targetSchema = ""; anExecutionStep.coordStepDetail.targetTable = ""; coordTempTableDropList = anExecutionStep.coordStepDetail.dropList; modifyFinalSelectOnCoordinator(anExecutionStep, aQueryPlan, TRANSFORM_PROJECTIONS); } } } } /** * Changes projections for final step when no group by is present. * * @param anExecutionStep * @param aQueryPlan * @param transformProjections */ private void modifyFinalSelectOnNodes(ExecutionStep anExecutionStep, QueryPlan aQueryPlan, boolean transformProjections) { StringBuilder finalSelect = new StringBuilder(256); finalSelect.append("SELECT "); if (aQueryPlan.isDistinct) { finalSelect.append("DISTINCT "); } if (aQueryPlan.orderByClause.length() > 0) { // To make sure projections are correct, do relation subquery if (transformProjections) { anExecutionStep.aStepDetail.queryString = finalSelect .append(aQueryPlan.finalProjString) .append(aQueryPlan.addedFinalProjections) .append(" FROM (") .append(anExecutionStep.aStepDetail.queryString) .append(") as axtmzyx") .append(" order by ") .append(aQueryPlan.orderByClause) .toString(); } else { anExecutionStep.aStepDetail.queryString = finalSelect .append(aQueryPlan.finalProjString) .append(aQueryPlan.addedFinalProjections) .append(" ") .append(anExecutionStep.aStepDetail.nonProjectionSelectPart) .append(" order by ") .append(aQueryPlan.orderByClause) .toString(); } } else { if (transformProjections) { // To make sure projections are correct, do relation subquery anExecutionStep.aStepDetail.queryString = finalSelect .append(aQueryPlan.finalProjString) .append(" FROM (") .append(anExecutionStep.aStepDetail.queryString) .append(") as xtmzyx") .toString(); } else { anExecutionStep.aStepDetail.queryString = finalSelect .append(aQueryPlan.finalProjString) .append(" ") .append(anExecutionStep.aStepDetail.nonProjectionSelectPart) .toString(); } } // Flag that we should "stream" and combine ResultSets on the fly this.combineResults = true; nodeTempTableDropList = anExecutionStep.aStepDetail.dropList; // Add limit and offset info addLimitAndOffset (anExecutionStep, aQueryPlan); addIntoTable(anExecutionStep.aStepDetail, anExecutionStep.aStepDetail, anExecutionStep.nodeUsageTable, aQueryPlan); } /** * If target table is specified modify final step to create final table * instead of sending results to the Coordinator * @param srcDetail * @param dstDetail * @param nodeUsageTable * @param aQueryPlan */ private void addIntoTable(StepDetail srcDetail, StepDetail dstDetail, Map<Integer, NodeUsage> nodeUsageTable, QueryPlan aQueryPlan) { SysTable intoTable = aQueryPlan.getIntoTable(); if (intoTable != null) { if (combineResults) { if ((aQueryPlan.getLimit() > 0 || aQueryPlan.getOffset() > 0) && nodeUsageTable.size() != 1) { return; } else { combineResults = false; } } // This is not initialized if query is like select count(*) ... srcDetail.targetTable = dstDetail.targetTable; srcDetail.consumerNodeList = new ArrayList<Integer>( intoTable.getPartitionMap().allPartitions().size()); // Share the list dstDetail.consumerNodeList = srcDetail.consumerNodeList; for (Integer nodeID : intoTable.getPartitionMap().allPartitions()) { srcDetail.consumerNodeList.add(nodeID); NodeUsage nu = nodeUsageTable.get(nodeID); if (nu == null) { nu = new NodeUsage(nodeID, false, false); nodeUsageTable.put(nodeID, nu); } nu.isConsumer = true; } dstDetail.isConsumer = true; if (intoTable.getPartitionScheme() == SysTable.PTYPE_HASH) { String hashColumn = intoTable.getPartitionColumn(); // SysColumn partColumn = intoTable.getPartitionedColumn(); // Possible if CREATE TABLE AS specifies non-existing column if (partColumn == null) { throw new XDBServerException("Partitioning column not found: " + hashColumn); } int partColumnPosition = intoTable.getSysColumn( partColumn.getColName()).getColSeq(); srcDetail.setDestTypeHash(partColumnPosition, intoTable.getPartitionMap()); srcDetail.setHashDataType(new ExpressionType(partColumn)); if (aQueryPlan.isExistingInto()) { srcDetail.targetTable = intoTable.getTableName(); srcDetail.targetSchema = null; } } else if (intoTable.getPartitionScheme() == SysTable.PTYPE_ROBIN) { Collection<Integer> parts = intoTable.getPartitionMap() .getPartitions(null); srcDetail.setDestTypeOne(parts.iterator().next()); if (aQueryPlan.isExistingInto()) { srcDetail.targetTable = intoTable.getTableName(); srcDetail.targetSchema = null; } } else { srcDetail.setDestTypeBroadcast(); // Required if target table is round robin, no harm otherwise srcDetail.setPartitionMap(intoTable.getPartitionMap()); } } } /** * Append LIMIT and OFFSET to step, if applicable * * @param anExecutionStep * @param aQueryPlan */ private void addLimitAndOffset (ExecutionStep anExecutionStep, QueryPlan aQueryPlan) { // Handle LIMIT and OFFSET for last step. // If there is only node being used, we do not use // CombinedResultSet object, so just pass along verbatim. if (anExecutionStep.nodeUsageTable.size() == 1) { if (aQueryPlan.getLimit() > -1) { anExecutionStep.aStepDetail.queryString += " LIMIT " + aQueryPlan.getLimit(); } if (aQueryPlan.getOffset() > -1) { anExecutionStep.aStepDetail.queryString += " OFFSET " + aQueryPlan.getOffset(); } } else { // more than 1 node. It is up to CombinedResultSet // to merge them if (aQueryPlan.getLimit() > -1) { if (aQueryPlan.getOffset() > -1) { // Add limit and offset together, // CombinedResultSet will take care of offset on merge anExecutionStep.aStepDetail.queryString += " LIMIT " + (aQueryPlan.getLimit() + aQueryPlan.getOffset()); } else { anExecutionStep.aStepDetail.queryString += " LIMIT " + aQueryPlan.getLimit(); } } finalLimit = aQueryPlan.getLimit(); finalOffset = aQueryPlan.getOffset(); } } public long getLimit() { return finalLimit; } public long getOffset() { return finalOffset; } /** * Modifies the final select, in case it is being executed on the * coordinator. * * @param anExecutionStep * @param aQueryPlan * @param transformProjections */ private void modifyFinalSelectOnCoordinator(ExecutionStep anExecutionStep, QueryPlan aQueryPlan, boolean transformProjections) { String finalSelect = "SELECT "; if (aQueryPlan.isDistinct) { finalSelect += "DISTINCT "; } if (aQueryPlan.orderByClause.length() > 0) { // To make sure projections are correct, do relation subquery if (transformProjections) { // I believe this is incorrect for unions // but we should move away from TRANSFORM_PROJECTIONS anExecutionStep.coordStepDetail.queryString = finalSelect + aQueryPlan.finalProjString + aQueryPlan.addedFinalProjections + " FROM (" + anExecutionStep.coordStepDetail.queryString + ")" + " order by " + aQueryPlan.orderByClause; } else { anExecutionStep.coordStepDetail.queryString = finalSelect + aQueryPlan.finalProjString + " " + anExecutionStep.coordStepDetail.nonProjectionSelectPart + " order by " + aQueryPlan.orderByClause; } } else { // To make sure projections are correct, do relation subquery if (transformProjections) { anExecutionStep.coordStepDetail.queryString = finalSelect + aQueryPlan.finalProjString + " FROM (" + anExecutionStep.coordStepDetail.queryString + ") as xtmzyx"; } else { anExecutionStep.coordStepDetail.queryString = finalSelect + aQueryPlan.finalProjString + " " + anExecutionStep.coordStepDetail.nonProjectionSelectPart; } } // Handle LIMIT and OFFSET. // We are just executing on the coordinator, so we can append here if (aQueryPlan.getLimit() > -1) { anExecutionStep.coordStepDetail.queryString += " LIMIT " + aQueryPlan.getLimit(); } if (aQueryPlan.getOffset() > -1) { anExecutionStep.coordStepDetail.queryString += " OFFSET " + aQueryPlan.getOffset(); } addIntoTable(anExecutionStep.coordStepDetail, anExecutionStep.aStepDetail, anExecutionStep.nodeUsageTable, aQueryPlan); } /** * Special handling if we are dealing with a UNION * * @param aQueryPlan * @param parentNodeUsageTable * @param parentTargetNodeList */ private void buildUnionPlan(QueryPlan aQueryPlan, HashMap<Integer, NodeUsage> parentNodeUsageTable, List<DBNode> parentTargetNodeList) { ExecutionPlan subExecPlan; ExecutionStep anExecutionStep; unionPlanList = new ArrayList<ExecutionPlan>(); nodeTempTableDropList = new ArrayList<String>(); coordTempTableDropList = new ArrayList<String>(); ArrayList<String> finalCoordTempTableDropList = new ArrayList<String>(); for (QueryPlan subQueryPlan : aQueryPlan.unionSubplanList) { // If there is an order by clause, it is ok to add it here if (subQueryPlan.orderByClause.length() > 0) { Leaf finalLeaf = subQueryPlan.getLastLeaf(); finalLeaf.setSelectStatement(finalLeaf.getSelectStatement() + " order by " + subQueryPlan.orderByClause); } subExecPlan = new ExecutionPlan(this, subQueryPlan, parentNodeUsageTable, parentTargetNodeList, true, client); unionPlanList.add(subExecPlan); // Make sure we drop temp tables properly ExecutionStep lastStep = subExecPlan.stepList.get(subExecPlan.stepList.size() - 1); if (lastStep.aStepDetail.isProducer) { nodeTempTableDropList.addAll(lastStep.aStepDetail.dropList); } else { if (lastStep.coordStepDetail != null) { coordTempTableDropList .addAll(lastStep.coordStepDetail.dropList); } } finalCoordTempTableDropList.add(subQueryPlan.finalTableName); // We also want to provide some additional information // about how we can combine the unions, for MultinodeExecutor. // See also QueryPlan.createPlanSegmentFromTree. // We should end up with 0 or more 1's, followed by // 0 or more 2's. if (subQueryPlan.unionType == QueryPlan.UNIONTYPE_UNIONALL) { if (lastStep.aStepDetail != null) { lastStep.aStepDetail.unionResultGroup = 2; } if (lastStep.coordStepDetail != null) { lastStep.coordStepDetail.unionResultGroup = 2; } } else { if (lastStep.aStepDetail != null) { lastStep.aStepDetail.unionResultGroup = 1; } if (lastStep.coordStepDetail != null) { lastStep.coordStepDetail.unionResultGroup = 1; } } } finalTempTableName = aQueryPlan.finalTableName; // Save order by clause for later finalProjString = aQueryPlan.finalProjString; if (!aQueryPlan.isTopLevelPlan) { // There will just be one Leaf, containing all subqueries /* * Note that we always have these run from the nodes, not the * coordinator. It is ok, because we previously did a hashed * distribution. That is also convenient for combining UNIONs, * independent if we have Q1 UNION Q2 UNION ALL Q3, the hash ensures * that each node will be dealing with unique data. */ Leaf aLeaf = aQueryPlan.getFirstLeaf(); anExecutionStep = createNewStep(); // We are a union query in a subquery. // See if the parent is also a union query and the top plan. // In that case, we flag that it is "final" if (parentExecutionPlan != null && parentExecutionPlan.parentExecutionPlan == null && !parentExecutionPlan.unionPlanList.isEmpty()) { // update Step, set nodeUsage properly anExecutionStep.convertFromLeaf(aLeaf, null, null, null, parentNodeUsageTable, parentTargetNodeList, false, aQueryPlan.planType); anExecutionStep.aStepDetail.setDestTypeCoordinatorFinal(); anExecutionStep.isFinalStep = true; anExecutionStep.aStepDetail.isFinalUnionPart = true; anExecutionStep.aStepDetail.finalUnionPartSortInfo = aQueryPlan.topQueryPlan.sortInfo; } else { // We are nested more deeply. // update Step, set nodeUsage properly anExecutionStep.convertFromLeaf(aLeaf, null, null, null, parentNodeUsageTable, parentTargetNodeList, true, aQueryPlan.planType); } anExecutionStep.aStepDetail.dropList .addAll(finalCoordTempTableDropList); } else { isTopLevelUnion = true; } } /** * Creates a new ExecutionStep, adds it to the step list and returns it. * @return */ private ExecutionStep createNewStep() { ExecutionStep anExecutionStep = new ExecutionStep(this, client); stepList.add(anExecutionStep); return anExecutionStep; } /** * Due to destination node issues, what this method does is step * through the ExecutionPlan in the same order that it should be executed, * and look for inconsistencies where the last step of subplan sends it to * the wrong destination. * * TODO: remove other code that determines destinations and use this * instead since it will overwrite those. * * @see correctStepDestinations */ public void correctDestinations() { correctStepDestinations(); } /** * Due to destination node issues, what this method does is step * through the ExecutionPlan in the same order that it should be executed, * and look for inconsistencies where the last step of subplan sends it to * the wrong destination. * * @see correctDestinations * @return the last ExecutionStep of the plan */ private ExecutionStep correctStepDestinations() { ExecutionStep lastExecStep = null; if (!unionPlanList.isEmpty()) { correctUnionQueryDestinations(); } // Check to see if we have any scalar subqueries and correct for (ExecutionPlan scalarPlan : scalarPlanList) { scalarPlan.correctStepDestinations(); } // correct any relation subplans for (ExecutionPlan relationPlan : relationPlanList) { // check last step ExecutionStep lastRelationStep = relationPlan .correctStepDestinations(); lastRelationStep.correctDestinations(stepList.get(0)); } // Now do the steps for (ExecutionStep anExecStep : stepList) { // See if we need to handle any correlated subplans first if (anExecStep.correlatedSubPlan != null) { // This may be unnecessary, but being safe anExecStep.correlatedSubPlan.correctStepDestinations(); } QueryPlan.CATEGORY_QUERYFLOW.info(anExecStep); // see if we have any uncorrelated subqueries if (anExecStep.uncorrelatedSubPlanList != null) { for (ExecutionPlan uncorSubPlan : anExecStep.uncorrelatedSubPlanList) { // need to set destination for this ExecutionStep uncorLastExecStep = uncorSubPlan .correctStepDestinations(); if (uncorLastExecStep != null) { uncorLastExecStep.correctDestinations(anExecStep); } } } // double check previous step with this one if (lastExecStep != null) { lastExecStep.correctDestinations(anExecStep); } lastExecStep = anExecStep; } return lastExecStep; } /** * Corrects destinations in UNION queries */ private void correctUnionQueryDestinations() { for (ExecutionPlan subPlan : unionPlanList) { subPlan.correctStepDestinations(); } } /** * Returns the top level plan * * @return the top level ExecutionPlan */ protected ExecutionPlan getTopParentPlan () { if (parentExecutionPlan == null) { return this; } else { return parentExecutionPlan.getTopParentPlan(); } } /** * Prepares PreparedStatement paramters based on the types. * * @param paramTypes - the parameter types used */ public void prepareParameters (int[] paramTypes) { anEPParameterHelper = new ExecutionPlanPreparedHandler(client, paramTypes); anEPParameterHelper.preparePlanParameters (this); } /** * Replaces the appropriate parameter values from the parameter list * for PreparedStatements * * @param paramValues - the values to substitute. */ public void substituteParameterValues (String[] paramValues) { anEPParameterHelper.substituteParameterValues(paramValues); } public boolean isSingleStep() { return (relationPlanList == null || relationPlanList.isEmpty()) && (scalarPlanList == null || scalarPlanList.isEmpty()) && (unionPlanList == null || unionPlanList.isEmpty()) && stepList != null && stepList.size() == 1; } public String toString() { StringBuffer sbPlan = new StringBuffer(1024); for (ExecutionPlan unionPlan : unionPlanList) { sbPlan.append("\n Union Subplan:\n"); sbPlan.append(unionPlan); } for (ExecutionPlan scalarPlan : scalarPlanList) { sbPlan.append("\n Scalar Subplan:\n"); sbPlan.append(scalarPlan); } for (ExecutionPlan relationPlan : relationPlanList) { sbPlan.append("\n Relation Subplan:\n"); sbPlan.append(relationPlan); } for (ExecutionStep execStep : stepList) { if (execStep.correlatedSubPlan != null) { sbPlan.append("\n Correlated Subplan:\n"); sbPlan.append(execStep.correlatedSubPlan); } if (execStep.uncorrelatedSubPlanList != null) { for (ExecutionPlan uncorSubPlan : execStep.uncorrelatedSubPlanList) { sbPlan.append("\n Uncorrelated Subplan:\n"); sbPlan.append(uncorSubPlan); } } if (execStep.outerSubPlan != null) { sbPlan.append("\n Outer Subplan:\n"); sbPlan.append(execStep.outerSubPlan); } sbPlan.append("\n"); sbPlan.append(execStep); if (execStep.correlatedSendDownStep != null) { sbPlan.append("\n"); sbPlan.append(execStep.correlatedSendDownStep); if (execStep.correlatedSendDownStep2 != null) { sbPlan.append("\n"); sbPlan.append(execStep.correlatedSendDownStep2); } } } return sbPlan.toString(); } }